Nette Documentation Preview

syntax
Struktura imenika aplikacije
****************************

<div class=perex>

Kako zasnovati jasno in razširljivo imeniško strukturo za projekte v okolju Nette Framework? Predstavili vam bomo preizkušene prakse, ki vam bodo pomagale organizirati kodo. Naučili se boste:

- kako **logično strukturirati** aplikacijo v imenike
- kako zasnovati strukturo, da se bo z rastjo projekta dobro **razširila**
- katere so **možne alternative** in njihove prednosti ali slabosti

</div>


Pomembno je omeniti, da samo ogrodje Nette Framework ne vztraja pri nobeni posebni strukturi. Zasnovan je tako, da ga je mogoče zlahka prilagoditi vsem potrebam in željam.


Osnovna struktura projekta .[#toc-basic-project-structure]
==========================================================

Čeprav ogrodje Nette Framework ne narekuje nobene fiksne strukture imenikov, obstaja preverjena privzeta ureditev v obliki [spletnega projekta |https://github.com/nette/web-project]:

/--pre
<b>web-project/</b>
├── <b>app/</b>              ← imenik aplikacij
├── <b>assets/</b>           ← SCSS, JS datoteke, slike..., lahko tudi resources/
├── <b>bin/</b>              ← skripte ukazne vrstice
├── <b>config/</b>           ← konfiguracija
├── <b>log/</b>              ← zabeležene napake
├── <b>temp/</b>             ← začasne datoteke, predpomnilnik
├── <b>tests/</b>            ← testi
├── <b>vendor/</b>           ← knjižnice, ki jih je namestil Composer
└── <b>www/</b>              ← javni imenik (document-root)
\--

To strukturo lahko poljubno spreminjate glede na svoje potrebe - preimenujete ali premikate mape. Nato morate le prilagoditi relativne poti do imenikov v `Bootstrap.php` in po možnosti `composer.json`. Nič drugega ni potrebno, nobene zapletene ponovne konfiguracije, nobenih stalnih sprememb. Nette ima pametno samodejno zaznavanje in samodejno prepozna lokacijo aplikacije, vključno z njeno bazo URL.


Načela organizacije kode .[#toc-code-organization-principles]
=============================================================

Ko prvič raziskujete nov projekt, se morate znati hitro orientirati. Predstavljajte si, da kliknete na imenik `app/Model/` in vidite to strukturo:

/--pre
<b>app/Model/</b>
├── <b>Services/</b>
├── <b>Repositories/</b>
└── <b>Entities/</b>
\--

Iz nje boste izvedeli le, da projekt uporablja nekatere storitve, skladišča in entitete. Ne boste izvedeli ničesar o dejanskem namenu aplikacije.

Oglejmo si drugačen pristop - **organizacija po domenah**:

/--pre
<b>app/Model/</b>
├── <b>Cart/</b>
├── <b>Payment/</b>
├── <b>Order/</b>
└── <b>Product/</b>
\--

Na prvi pogled je jasno, da gre za spletno mesto e-trgovine. Že imena imenikov razkrivajo, kaj aplikacija zmore - dela s plačili, naročili in izdelki.

Prvi pristop (organizacija po vrsti razreda) v praksi prinaša več težav: koda, ki je logično povezana, je razpršena po različnih mapah in med njimi je treba preskakovati. Zato se bomo organizirali po domenah.


Prostori imen .[#toc-namespaces]
--------------------------------

Običajno struktura imenikov ustreza imenskim prostorom v aplikaciji. To pomeni, da fizična lokacija datotek ustreza njihovemu imenskemu prostoru. Na primer, razred, ki se nahaja v `app/Model/Product/ProductRepository.php`, mora imeti imenski prostor `App\Model\Product`. To načelo pomaga pri usmerjanju kode in poenostavlja samodejno nalaganje.


Ednina in množina v imenih .[#toc-singular-vs-plural-in-names]
--------------------------------------------------------------

Opazite, da za glavne imenike aplikacij uporabljamo ednino: `app` `config` , `log`, `temp`, `www`. Enako velja tudi znotraj aplikacije: `Model`, `Core`, `Presentation`. To je zato, ker vsak od njih predstavlja en enoten koncept.

Podobno tudi `app/Model/Product` predstavlja vse o izdelkih. Ne imenujemo je `Products`, ker to ni mapa, polna izdelkov (ki bi vsebovala datoteke, kot so `iphone.php`, `samsung.php`). To je imenski prostor, ki vsebuje razrede za delo z izdelki - `ProductRepository.php`, `ProductService.php`.

Mapa `app/Tasks` je množinska, ker vsebuje nabor samostojnih izvršilnih skript - `CleanupTask.php`, `ImportTask.php`. Vsaka od njih je samostojna enota.

Zaradi doslednosti priporočamo uporabo:
- ednino za imenske prostore, ki predstavljajo funkcionalno enoto (tudi če delate z več enotami)
- množino za zbirke neodvisnih enot
- V primeru negotovosti ali če o tem ne želite razmišljati, izberite ednino


Javni imenik `www/` .[#toc-public-directory-www]
================================================

Ta imenik je edini, ki je dostopen s spleta (tako imenovani koren dokumentov). Pogosto lahko namesto imena `www/` naletite na ime `public/` - to je le stvar konvencije in ne vpliva na funkcionalnost. Imenik vsebuje:
- [vstopno točko |bootstrap#index.php] aplikacije `index.php`
- datoteko `.htaccess` s pravili mod_rewrite (za Apache)
- statične datoteke (CSS, JavaScript, slike)
- naložene datoteke

Za ustrezno varnost aplikacije je ključnega pomena, da je pravilno [konfiguriran document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url].

.[note]
V ta imenik nikoli ne postavite mape `node_modules/` - vsebuje na tisoče datotek, ki so lahko izvedljive in ne smejo biti javno dostopne.


Imenik aplikacij `app/` .[#toc-application-directory-app]
=========================================================

To je glavni imenik z aplikacijsko kodo. Osnovna struktura:

/--pre
<b>app/</b>
├── <b>Core/</b>               ← infrastrukturne zadeve
├── <b>Model/</b>              ← poslovna logika
├── <b>Presentation/</b>       ← predstavitve in predloge
├── <b>Tasks/</b>              ← skripte ukazov
└── <b>Bootstrap.php</b>       ← zagonski razred aplikacije
\--

`Bootstrap.php` je [zagonski razred aplikacije, |bootstrap] ki inicializira okolje, naloži konfiguracijo in ustvari vsebnik DI.

Zdaj si podrobno oglejmo posamezne podimenike.


Predstavniki in predloge .[#toc-presenters-and-templates]
=========================================================

Predstavitveni del aplikacije imamo v imeniku `app/Presentation`. Druga možnost je kratek naslov `app/UI`. To je prostor za vse predstavitve, njihove predloge in morebitne pomožne razrede.

To plast organiziramo po domenah. V kompleksnem projektu, ki združuje e-trgovino, blog in API, bi bila struktura videti takole:

/--pre
<b>app/Presentation/</b>
├── <b>Shop/</b>              ← e-trgovina frontend
│   ├── <b>Product/</b>
│   ├── <b>Cart/</b>
│   └── <b>Order/</b>
├── <b>Blog/</b>              ← blog
│   ├── <b>Home/</b>
│   └── <b>Post/</b>
├── <b>Admin/</b>             ← uprava
│   ├── <b>Dashboard/</b>
│   └── <b>Products/</b>
└── <b>Api/</b>               ← Končne točke API
	└── <b>V1/</b>
\--

Za preprost blog pa bi uporabili to strukturo:

/--pre
<b>app/Presentation/</b>
├── <b>Front/</b>             ← spletna stran frontend
│   ├── <b>Home/</b>
│   └── <b>Post/</b>
├── <b>Admin/</b>             ← uprava
│   ├── <b>Dashboard/</b>
│   └── <b>Posts/</b>
├── <b>Error/</b>
└── <b>Export/</b>            ← RSS, zemljevide itd.
\--

V mapah, kot sta `Home/` ali `Dashboard/`, so predstavniki in predloge. Mape, kot so `Front/`, `Admin/` ali `Api/`, se imenujejo **moduli**. Tehnično gledano so to običajni imeniki, ki služijo za logično organizacijo aplikacije.

Vsaka mapa s predstavnikom vsebuje podobno poimenovane predstavnike in njihove predloge. Na primer, mapa `Dashboard/` vsebuje:

/--pre
<b>Dashboard/</b>
├── <b>DashboardPresenter.php</b>     ← voditelj
└── <b>default.latte</b>              ← predloga
\--

Ta struktura imenikov se odraža v imenskih prostorih razredov. Na primer, `DashboardPresenter` je v imenskem prostoru `App\Presentation\Admin\Dashboard` (glejte [preslikavo predavateljev |#presenter mapping]):

```php
namespace App\Presentation\Admin\Dashboard;

class DashboardPresenter extends Nette\Application\UI\Presenter
{
	//...
}
```

Predstavnik `Dashboard` znotraj modula `Admin` v aplikaciji označujemo z zapisom v dvopičju kot `Admin:Dashboard`. Na njegovo akcijo `default` pa nato kot `Admin:Dashboard:default`. Za vgnezdene module uporabljamo več dvopičij, na primer `Shop:Order:Detail:default`.


Razvoj prilagodljive strukture .[#toc-flexible-structure-development]
---------------------------------------------------------------------

Ena od velikih prednosti te strukture je, kako elegantno se prilagaja naraščajočim potrebam projekta. Kot primer vzemimo del, ki ustvarja vire XML. Na začetku imamo preprost obrazec:

/--pre
<b>Export/</b>
├── <b>ExportPresenter.php</b>   ← en predavatelj za ves izvoz
├── <b>sitemap.latte</b>         ← predlogo za karto spletnega mesta
└── <b>feed.latte</b>            ← predlogo za vir RSS
\--

Sčasoma se doda več vrst virov in zanje potrebujemo več logike... Ni problema! Mapa `Export/` preprosto postane modul:

/--pre
<b>Export/</b>
├── <b>Sitemap/</b>
│   ├── <b>SitemapPresenter.php</b>
│   └── <b>sitemap.latte</b>
└── <b>Feed/</b>
	├── <b>FeedPresenter.php</b>
	├── <b>amazon.latte</b>         ← krma za Amazon
	└── <b>ebay.latte</b>           ← vir za eBay
\--

Preoblikovanje je popolnoma nemoteno - ustvarite nove podmape, razdelite kodo vanje in posodobite povezave (npr. iz `Export:feed` v `Export:Feed:amazon`). Zaradi tega lahko strukturo postopoma širimo po potrebi, raven gnezdenja ni v ničemer omejena.

Če imate na primer v administraciji veliko predstavnikov, povezanih z upravljanjem naročil, kot so `OrderDetail`, `OrderEdit`, `OrderDispatch` itd, lahko za boljšo organizacijo ustvarite modul (mapo) `Order`, ki bo vseboval (mape za) predstavnike `Detail`, `Edit`, `Dispatch` in druge.


Lokacija predloge .[#toc-template-location]
-------------------------------------------

V prejšnjih primerih smo videli, da se predloge nahajajo neposredno v mapi s predstavitvijo:

/--pre
<b>Dashboard/</b>
├── <b>DashboardPresenter.php</b>     ← voditelj
├── <b>DashboardTemplate.php</b>      ← neobvezen razred predloge
└── <b>default.latte</b>              ← Predloga
\--

Ta lokacija se v praksi izkaže za najprimernejšo - vse povezane datoteke imate takoj pri roki.

Predloge lahko namestite tudi v podmapo `templates/`. Nette podpira obe različici. Predloge lahko postavite tudi povsem zunaj mape `Presentation/`. Vse o možnostih lokacije predlog najdete v poglavju [Iskanje predlog |templates#Template Lookup].


Pomožni razredi in komponente .[#toc-helper-classes-and-components]
-------------------------------------------------------------------

Predstavniki in predloge so pogosto opremljeni z drugimi pomožnimi datotekami. Razporedimo jih logično glede na njihovo področje uporabe:

1. **Neposredno s predstavitvijo** v primeru posebnih komponent za določeno predstavitev:

/--pre
<b>Product/</b>
├── <b>ProductPresenter.php</b>
├── <b>ProductGrid.php</b>        ← komponenta za uvrstitev izdelka na seznam
└── <b>FilterForm.php</b>         ← obrazec za filtriranje
\--

2. **Za modul** - priporočamo uporabo mape `Accessory`, ki je lepo postavljena na začetek abecede:

/--pre
<b>Front/</b>
├── <b>Accessory/</b>
│   ├── <b>NavbarControl.php</b>    ← komponente za frontend
│   └── <b>TemplateFilters.php</b>
├── <b>Product/</b>
└── <b>Cart/</b>
\--

3. **Za celotno aplikacijo** - v `Presentation/Accessory/`:
/--pre
<b>app/Presentation/</b>
├── <b>Accessory/</b>
│   ├── <b>LatteExtension.php</b>
│   └── <b>TemplateFilters.php</b>
├── <b>Front/</b>
└── <b>Admin/</b>
\--

Lahko pa pomožne razrede, kot sta `LatteExtension.php` ali `TemplateFilters.php`, namestite v infrastrukturno mapo `app/Core/Latte/`. Komponente pa v mapo `app/Components`. Izbira je odvisna od skupinskih konvencij.


Model - srce aplikacije .[#toc-model-heart-of-the-application]
==============================================================

Model vsebuje vso poslovno logiko aplikacije. Za njegovo organizacijo velja enako pravilo - strukturiramo ga po domenah:

/--pre
<b>app/Model/</b>
├── <b>Payment/</b>                   ← vse o plačilih
│   ├── <b>PaymentFacade.php</b>      ← glavna vstopna točka
│   ├── <b>PaymentRepository.php</b>
│   ├── <b>Payment.php</b>            ← entiteta
├── <b>Order/</b>                     ← vse o naročilih
│   ├── <b>OrderFacade.php</b>
│   ├── <b>OrderRepository.php</b>
│   ├── <b>Order.php</b>
└── <b>Shipping/</b>                  ← vse o odpremi
\--

V modelu se običajno srečamo s temi vrstami razredov:

**Fasade**: predstavljajo glavno vstopno točko v določeno domeno v aplikaciji. Delujejo kot orkestrator, ki usklajuje sodelovanje med različnimi storitvami za izvajanje celotnih primerov uporabe (kot sta "ustvariti naročilo" ali "obdelati plačilo"). Pod orkestracijsko plastjo fasada skriva podrobnosti izvajanja pred preostalim delom aplikacije in tako zagotavlja čist vmesnik za delo z določeno domeno.

```php
class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// potrjevanje
		// ustvarjanje naročila.
		// pošiljanje e-pošte
		// pisanje v statistiko
	}
}
```

**Službe**: osredotočajo se na posebne poslovne operacije znotraj domene. Za razliko od fasad, ki orkestrirajo celotne primere uporabe, storitev izvaja specifično poslovno logiko (kot so izračuni cen ali obdelava plačil). Storitve so običajno brez stanja in jih lahko uporabljajo fasade kot gradnike za kompleksnejše operacije ali drugi deli aplikacije neposredno za enostavnejša opravila.

```php
class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// izračun cene
	}
}
```

**Hrambe**: skrbijo za vso komunikacijo s hrambo podatkov, običajno s podatkovno zbirko. Njihova naloga je nalaganje in shranjevanje entitet ter izvajanje metod za njihovo iskanje. Skladišče ščiti preostalo aplikacijo pred podrobnostmi izvajanja podatkovne zbirke in zagotavlja objektno usmerjen vmesnik za delo s podatki.

```php
class OrderRepository
{
	public function find(int $id): ?Order
	{
	}

	public function findByCustomer(int $customerId): array
	{
	}
}
```

**Entitete**: objekti, ki predstavljajo glavne poslovne koncepte v aplikaciji, ki imajo svojo identiteto in se s časom spreminjajo. Običajno so to razredi, ki so s pomočjo ORM (kot sta Nette Database Explorer ali Doctrine) preslikani v tabele podatkovne zbirke. Entitete lahko vsebujejo poslovna pravila v zvezi z njihovimi podatki in logiko potrjevanja.

```php
// Entiteta, preslikana v tabelo naročil v zbirki podatkov
class Order extends Nette\Database\Table\ActiveRow
{
	public function addItem(Product $product, int $quantity): void
	{
		$this->related('order_items')->insert([
			'product_id' => $product->id,
			'quantity' => $quantity,
			'unit_price' => $product->price,
		]);
	}
}
```

**Vrednostni objekti**: nespremenljivi objekti, ki predstavljajo vrednosti brez lastne identitete - na primer znesek denarja ali e-poštni naslov. Dva primerka objekta vrednosti z enakimi vrednostmi veljata za enaka.


Koda infrastrukture .[#toc-infrastructure-code]
===============================================

V mapi `Core/` (ali tudi `Infrastructure/`) se nahaja tehnični temelj aplikacije. Infrastrukturna koda običajno vključuje:

/--pre
<b>app/Core/</b>
├── <b>Router/</b>               ← usmerjanje in upravljanje URL.
│   └── <b>RouterFactory.php</b>
├── <b>Security/</b>             ← avtentikacija in avtorizacija.
│   ├── <b>Authenticator.php</b>
│   └── <b>Authorizator.php</b>
├── <b>Logging/</b>              ← beleženje in spremljanje
│   ├── <b>SentryLogger.php</b>
│   └── <b>FileLogger.php
├── <b>Cache/</b>                ← plast predpomnilnika
│   └── <b>FullPageCache.php</b>
└── <b>Integration/</b>          ← integracija z zunanjimi storitvami
	├── <b>Slack/</b>
	└── <b>Stripe/</b>
\--

Za manjše projekte seveda zadostuje ravna struktura:

/--pre
<b>Core/</b>
├── <b>RouterFactory.php</b>
├── <b>Authenticator.php</b>
└── <b>QueueMailer.php</b>
\--

To je koda, ki:

- Obravnava tehnično infrastrukturo (usmerjanje, beleženje, predpomnilnik).
- vključuje zunanje storitve (Sentry, Elasticsearch, Redis)
- zagotavlja osnovne storitve za celotno aplikacijo (pošta, zbirka podatkov)
- je večinoma neodvisna od določene domene - predpomnilnik ali logger deluje enako za e-trgovino ali blog.

Se sprašujete, ali določen razred spada sem ali v model? Ključna razlika je v tem, da je koda v `Core/`:

- ne ve ničesar o domeni (izdelki, naročila, članki)
- Običajno jo je mogoče prenesti v drug projekt
- rešuje "kako deluje" (kako poslati pošto) in ne "kaj počne" (kakšno pošto poslati)

Primer za boljše razumevanje:

- `App\Core\MailerFactory` - ustvari primerke razreda za pošiljanje e-pošte, skrbi za nastavitve SMTP
- `App\Model\OrderMailer` - uporablja `MailerFactory` za pošiljanje e-poštnih sporočil o naročilih, pozna njihove predloge in ve, kdaj jih je treba poslati


Skripte ukazov .[#toc-command-scripts]
======================================

Aplikacije morajo pogosto opravljati naloge zunaj običajnih zahtevkov HTTP - bodisi gre za obdelavo podatkov v ozadju, vzdrževanje ali periodična opravila. Za izvajanje se uporabljajo preproste skripte v imeniku `bin/`, medtem ko je dejanska izvedbena logika nameščena v imeniku `app/Tasks/` (ali `app/Commands/`).

Primer:

/--pre
<b>app/Tasks/</b>
├── <b>Maintenance/</b>               ← skripte za vzdrževanje
│   ├── <b>CleanupCommand.php</b>     ← brisanje starih podatkov
│   └── <b>DbOptimizeCommand.php</b>  ← optimizacija podatkovne zbirke
├── <b>Integration/</b>               ← integracija z zunanjimi sistemi
│   ├── <b>ImportProducts.php</b>     ← uvoz iz sistema dobavitelja
│   └── <b>SyncOrders.php</b>         ← sinhronizacija naročil
└── <b>Scheduled/</b>                 ← redna opravila
	├── <b>NewsletterCommand.php</b>  ← pošiljanje novic
	└── <b>ReminderCommand.php</b>    ← obveščanje strank
\--

Kaj spada v model in kaj v ukazne skripte? Na primer, logika za pošiljanje enega e-poštnega sporočila je del modela, množično pošiljanje tisočih e-poštnih sporočil pa spada v `Tasks/`.

Opravila se običajno [izvajajo iz ukazne vrstice |https://blog.nette.org/en/cli-scripts-in-nette-application] ali prek programa cron. Lahko se zaženejo tudi prek zahteve HTTP, vendar je pri tem treba upoštevati varnost. Predstavnik, ki izvaja opravilo, mora biti zavarovan, na primer samo za prijavljene uporabnike ali z močnim žetonom in dostopom z dovoljenih naslovov IP. Za dolga opravila je treba povečati časovno omejitev skripta in uporabiti spletno stran `session_write_close()`, da se prepreči zaklepanje seje.


Drugi možni imeniki .[#toc-other-possible-directories]
======================================================

Poleg omenjenih osnovnih imenikov lahko glede na potrebe projekta dodate tudi druge specializirane mape. Oglejmo si najpogostejše in njihovo uporabo:

/--pre
<b>app/</b>
├── <b>Api/</b>              ← Logika API je neodvisna od predstavitvene plasti
├── <b>Database/</b>         ← migracijske skripte in sejalnike za testne podatke.
├── <b>Components/</b>       ← skupne vizualne komponente v aplikaciji
├── <b>Event/</b>            ← uporabno, če uporabljate arhitekturo, ki temelji na dogodkih
├── <b>Mail/</b>             ← e-poštne predloge in povezana logika
└── <b>Utils/</b>            ← pomožni razredi
\--

Za skupne vizualne komponente, ki se uporabljajo v predstavitvah v celotni aplikaciji, lahko uporabite mapo `app/Components` ali `app/Controls`:

/--pre
<b>app/Components/</b>
├── <b>Form/</b>                 ← komponente obrazca v skupni rabi
│   ├── <b>SignInForm.php</b>
│   └── <b>UserForm.php</b>
├── <b>Grid/</b>                 ← komponente za izpise podatkov
│   └── <b>DataGrid.php</b>
└── <b>Navigation/</b>           ← navigacijski elementi
	├── <b>Breadcrumbs.php</b>
	└── <b>Menu.php</b>
\--

V to mapo spadajo komponente z bolj zapleteno logiko. Če želite komponente deliti med več projekti, jih je dobro ločiti v samostojni paket Composer.

V imenik `app/Mail` lahko umestite upravljanje komunikacije z elektronsko pošto:

/--pre
<b>app/Mail/</b>
├── <b>templates/</b>            ← e-poštne predloge
│   ├── <b>order-confirmation.latte</b>
│   └── <b>welcome.latte</b>
└── <b>OrderMailer.php</b>
\--


Predavatelj Mapiranje .[#toc-presenter-mapping]
===============================================

Mapiranje določa pravila za izpeljavo imen razredov iz imen predvajalnikov. Določimo jih v [konfiguraciji |configuration] pod ključem `application › mapping`.

Na tej strani smo pokazali, da predstavnike namestimo v mapo `app/Presentation` (ali `app/UI`). V konfiguracijski datoteki moramo Nette obvestiti o tej konvenciji. Zadostuje ena vrstica:

```neon
application:
	mapping: App\Presentation\*\**Presenter
```

Kako deluje preslikava? Za boljše razumevanje si najprej predstavljajmo aplikacijo brez modulov. Želimo, da razredi predstavnikov spadajo v imenski prostor `App\Presentation`, tako da se predstavnik `Home` preslika v razred `App\Presentation\HomePresenter`. To dosežemo s to konfiguracijo:

```neon
application:
	mapping: App\Presentation\*Presenter
```

Mapiranje poteka tako, da se zvezdica v maski `App\Presentation\*Presenter` zamenja z imenom predstavnika `Home`, s čimer dobimo končno ime razreda `App\Presentation\HomePresenter`. Enostavno!

Vendar, kot vidite v primerih v tem in drugih poglavjih, umeščamo predstavitvene razrede v istoimenske podimenike, na primer predstavitveni razred `Home` se preslika v razred `App\Presentation\Home\HomePresenter`. To dosežemo s podvojitvijo dvopičja (zahteva aplikacijo Nette 3.2):

```neon
application:
	mapping: App\Presentation\**Presenter
```

Zdaj bomo prešli na preslikavo predstavnikov v module. Za vsak modul lahko določimo posebno kartiranje:

```neon
application:
	mapping:
		Front: App\Presentation\Front\**Presenter
		Admin: App\Presentation\Admin\**Presenter
		Api: App\Api\*Presenter
```

V skladu s to konfiguracijo je predstavnik `Front:Home` preslikan v razred `App\Presentation\Front\Home\HomePresenter`, medtem ko je predstavnik `Api:OAuth` preslikan v razred `App\Api\OAuthPresenter`.

Ker imata modula `Front` in `Admin` podoben način preslikave in ker bo takih modulov verjetno več, je mogoče ustvariti splošno pravilo, ki jih bo nadomestilo. V masko razreda bo dodana nova zvezdica za modul:

```neon
application:
	mapping:
		*: App\Presentation\*\**Presenter
		Api: App\Api\*Presenter
```

Deluje tudi za globlje ugnezdene imeniške strukture, kot je predstavnik `Admin:User:Edit`, kjer se segment z zvezdico ponovi za vsako raven in ima za rezultat razred `App\Presentation\Admin\User\Edit\EditPresenter`.

Alternativni zapis je, da namesto niza uporabimo polje, sestavljeno iz treh segmentov. Ta zapis je enakovreden prejšnjemu:

```neon
application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]
```

Struktura imenika aplikacije

Kako zasnovati jasno in razširljivo imeniško strukturo za projekte v okolju Nette Framework? Predstavili vam bomo preizkušene prakse, ki vam bodo pomagale organizirati kodo. Naučili se boste:

  • kako logično strukturirati aplikacijo v imenike
  • kako zasnovati strukturo, da se bo z rastjo projekta dobro razširila
  • katere so možne alternative in njihove prednosti ali slabosti

Pomembno je omeniti, da samo ogrodje Nette Framework ne vztraja pri nobeni posebni strukturi. Zasnovan je tako, da ga je mogoče zlahka prilagoditi vsem potrebam in željam.

Osnovna struktura projekta

Čeprav ogrodje Nette Framework ne narekuje nobene fiksne strukture imenikov, obstaja preverjena privzeta ureditev v obliki spletnega projekta:

web-project/
├── app/              ← imenik aplikacij
├── assets/           ← SCSS, JS datoteke, slike..., lahko tudi resources/
├── bin/              ← skripte ukazne vrstice
├── config/           ← konfiguracija
├── log/              ← zabeležene napake
├── temp/             ← začasne datoteke, predpomnilnik
├── tests/            ← testi
├── vendor/           ← knjižnice, ki jih je namestil Composer
└── www/              ← javni imenik (document-root)

To strukturo lahko poljubno spreminjate glede na svoje potrebe – preimenujete ali premikate mape. Nato morate le prilagoditi relativne poti do imenikov v Bootstrap.php in po možnosti composer.json. Nič drugega ni potrebno, nobene zapletene ponovne konfiguracije, nobenih stalnih sprememb. Nette ima pametno samodejno zaznavanje in samodejno prepozna lokacijo aplikacije, vključno z njeno bazo URL.

Načela organizacije kode

Ko prvič raziskujete nov projekt, se morate znati hitro orientirati. Predstavljajte si, da kliknete na imenik app/Model/ in vidite to strukturo:

app/Model/
├── Services/
├── Repositories/
└── Entities/

Iz nje boste izvedeli le, da projekt uporablja nekatere storitve, skladišča in entitete. Ne boste izvedeli ničesar o dejanskem namenu aplikacije.

Oglejmo si drugačen pristop – organizacija po domenah:

app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/

Na prvi pogled je jasno, da gre za spletno mesto e-trgovine. Že imena imenikov razkrivajo, kaj aplikacija zmore – dela s plačili, naročili in izdelki.

Prvi pristop (organizacija po vrsti razreda) v praksi prinaša več težav: koda, ki je logično povezana, je razpršena po različnih mapah in med njimi je treba preskakovati. Zato se bomo organizirali po domenah.

Prostori imen

Običajno struktura imenikov ustreza imenskim prostorom v aplikaciji. To pomeni, da fizična lokacija datotek ustreza njihovemu imenskemu prostoru. Na primer, razred, ki se nahaja v app/Model/Product/ProductRepository.php, mora imeti imenski prostor App\Model\Product. To načelo pomaga pri usmerjanju kode in poenostavlja samodejno nalaganje.

Ednina in množina v imenih

Opazite, da za glavne imenike aplikacij uporabljamo ednino: app config , log, temp, www. Enako velja tudi znotraj aplikacije: Model, Core, Presentation. To je zato, ker vsak od njih predstavlja en enoten koncept.

Podobno tudi app/Model/Product predstavlja vse o izdelkih. Ne imenujemo je Products, ker to ni mapa, polna izdelkov (ki bi vsebovala datoteke, kot so iphone.php, samsung.php). To je imenski prostor, ki vsebuje razrede za delo z izdelki – ProductRepository.php, ProductService.php.

Mapa app/Tasks je množinska, ker vsebuje nabor samostojnih izvršilnih skript – CleanupTask.php, ImportTask.php. Vsaka od njih je samostojna enota.

Zaradi doslednosti priporočamo uporabo:

  • ednino za imenske prostore, ki predstavljajo funkcionalno enoto (tudi če delate z več enotami)
  • množino za zbirke neodvisnih enot
  • V primeru negotovosti ali če o tem ne želite razmišljati, izberite ednino

Javni imenik www/

Ta imenik je edini, ki je dostopen s spleta (tako imenovani koren dokumentov). Pogosto lahko namesto imena www/ naletite na ime public/ – to je le stvar konvencije in ne vpliva na funkcionalnost. Imenik vsebuje:

  • vstopno točko aplikacije index.php
  • datoteko .htaccess s pravili mod_rewrite (za Apache)
  • statične datoteke (CSS, JavaScript, slike)
  • naložene datoteke

Za ustrezno varnost aplikacije je ključnega pomena, da je pravilno konfiguriran document-root.

V ta imenik nikoli ne postavite mape node_modules/ – vsebuje na tisoče datotek, ki so lahko izvedljive in ne smejo biti javno dostopne.

Imenik aplikacij app/

To je glavni imenik z aplikacijsko kodo. Osnovna struktura:

app/
├── Core/               ← infrastrukturne zadeve
├── Model/              ← poslovna logika
├── Presentation/       ← predstavitve in predloge
├── Tasks/              ← skripte ukazov
└── Bootstrap.php       ← zagonski razred aplikacije

Bootstrap.php je zagonski razred aplikacije, ki inicializira okolje, naloži konfiguracijo in ustvari vsebnik DI.

Zdaj si podrobno oglejmo posamezne podimenike.

Predstavniki in predloge

Predstavitveni del aplikacije imamo v imeniku app/Presentation. Druga možnost je kratek naslov app/UI. To je prostor za vse predstavitve, njihove predloge in morebitne pomožne razrede.

To plast organiziramo po domenah. V kompleksnem projektu, ki združuje e-trgovino, blog in API, bi bila struktura videti takole:

app/Presentation/
├── Shop/              ← e-trgovina frontend
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← blog
│   ├── Home/
│   └── Post/
├── Admin/             ← uprava
│   ├── Dashboard/
│   └── Products/
└── Api/               ← Končne točke API
	└── V1/

Za preprost blog pa bi uporabili to strukturo:

app/Presentation/
├── Front/             ← spletna stran frontend
│   ├── Home/
│   └── Post/
├── Admin/             ← uprava
│   ├── Dashboard/
│   └── Posts/
├── Error/
└── Export/            ← RSS, zemljevide itd.

V mapah, kot sta Home/ ali Dashboard/, so predstavniki in predloge. Mape, kot so Front/, Admin/ ali Api/, se imenujejo moduli. Tehnično gledano so to običajni imeniki, ki služijo za logično organizacijo aplikacije.

Vsaka mapa s predstavnikom vsebuje podobno poimenovane predstavnike in njihove predloge. Na primer, mapa Dashboard/ vsebuje:

Dashboard/
├── DashboardPresenter.php     ← voditelj
└── default.latte              ← predloga

Ta struktura imenikov se odraža v imenskih prostorih razredov. Na primer, DashboardPresenter je v imenskem prostoru App\Presentation\Admin\Dashboard (glejte preslikavo predavateljev):

namespace App\Presentation\Admin\Dashboard;

class DashboardPresenter extends Nette\Application\UI\Presenter
{
	//...
}

Predstavnik Dashboard znotraj modula Admin v aplikaciji označujemo z zapisom v dvopičju kot Admin:Dashboard. Na njegovo akcijo default pa nato kot Admin:Dashboard:default. Za vgnezdene module uporabljamo več dvopičij, na primer Shop:Order:Detail:default.

Razvoj prilagodljive strukture

Ena od velikih prednosti te strukture je, kako elegantno se prilagaja naraščajočim potrebam projekta. Kot primer vzemimo del, ki ustvarja vire XML. Na začetku imamo preprost obrazec:

Export/
├── ExportPresenter.php   ← en predavatelj za ves izvoz
├── sitemap.latte         ← predlogo za karto spletnega mesta
└── feed.latte            ← predlogo za vir RSS

Sčasoma se doda več vrst virov in zanje potrebujemo več logike… Ni problema! Mapa Export/ preprosto postane modul:

Export/
├── Sitemap/
│   ├── SitemapPresenter.php
│   └── sitemap.latte
└── Feed/
	├── FeedPresenter.php
	├── amazon.latte         ← krma za Amazon
	└── ebay.latte           ← vir za eBay

Preoblikovanje je popolnoma nemoteno – ustvarite nove podmape, razdelite kodo vanje in posodobite povezave (npr. iz Export:feed v Export:Feed:amazon). Zaradi tega lahko strukturo postopoma širimo po potrebi, raven gnezdenja ni v ničemer omejena.

Če imate na primer v administraciji veliko predstavnikov, povezanih z upravljanjem naročil, kot so OrderDetail, OrderEdit, OrderDispatch itd, lahko za boljšo organizacijo ustvarite modul (mapo) Order, ki bo vseboval (mape za) predstavnike Detail, Edit, Dispatch in druge.

Lokacija predloge

V prejšnjih primerih smo videli, da se predloge nahajajo neposredno v mapi s predstavitvijo:

Dashboard/
├── DashboardPresenter.php     ← voditelj
├── DashboardTemplate.php      ← neobvezen razred predloge
└── default.latte              ← Predloga

Ta lokacija se v praksi izkaže za najprimernejšo – vse povezane datoteke imate takoj pri roki.

Predloge lahko namestite tudi v podmapo templates/. Nette podpira obe različici. Predloge lahko postavite tudi povsem zunaj mape Presentation/. Vse o možnostih lokacije predlog najdete v poglavju Iskanje predlog.

Pomožni razredi in komponente

Predstavniki in predloge so pogosto opremljeni z drugimi pomožnimi datotekami. Razporedimo jih logično glede na njihovo področje uporabe:

1. Neposredno s predstavitvijo v primeru posebnih komponent za določeno predstavitev:

Product/
├── ProductPresenter.php
├── ProductGrid.php        ← komponenta za uvrstitev izdelka na seznam
└── FilterForm.php         ← obrazec za filtriranje

2. Za modul – priporočamo uporabo mape Accessory, ki je lepo postavljena na začetek abecede:

Front/
├── Accessory/
│   ├── NavbarControl.php    ← komponente za frontend
│   └── TemplateFilters.php
├── Product/
└── Cart/

3. Za celotno aplikacijo – v Presentation/Accessory/:

app/Presentation/
├── Accessory/
│   ├── LatteExtension.php
│   └── TemplateFilters.php
├── Front/
└── Admin/

Lahko pa pomožne razrede, kot sta LatteExtension.php ali TemplateFilters.php, namestite v infrastrukturno mapo app/Core/Latte/. Komponente pa v mapo app/Components. Izbira je odvisna od skupinskih konvencij.

Model – srce aplikacije

Model vsebuje vso poslovno logiko aplikacije. Za njegovo organizacijo velja enako pravilo – strukturiramo ga po domenah:

app/Model/
├── Payment/                   ← vse o plačilih
│   ├── PaymentFacade.php      ← glavna vstopna točka
│   ├── PaymentRepository.php
│   ├── Payment.php            ← entiteta
├── Order/                     ← vse o naročilih
│   ├── OrderFacade.php
│   ├── OrderRepository.php
│   ├── Order.php
└── Shipping/                  ← vse o odpremi

V modelu se običajno srečamo s temi vrstami razredov:

Fasade: predstavljajo glavno vstopno točko v določeno domeno v aplikaciji. Delujejo kot orkestrator, ki usklajuje sodelovanje med različnimi storitvami za izvajanje celotnih primerov uporabe (kot sta „ustvariti naročilo“ ali „obdelati plačilo“). Pod orkestracijsko plastjo fasada skriva podrobnosti izvajanja pred preostalim delom aplikacije in tako zagotavlja čist vmesnik za delo z določeno domeno.

class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// potrjevanje
		// ustvarjanje naročila.
		// pošiljanje e-pošte
		// pisanje v statistiko
	}
}

Službe: osredotočajo se na posebne poslovne operacije znotraj domene. Za razliko od fasad, ki orkestrirajo celotne primere uporabe, storitev izvaja specifično poslovno logiko (kot so izračuni cen ali obdelava plačil). Storitve so običajno brez stanja in jih lahko uporabljajo fasade kot gradnike za kompleksnejše operacije ali drugi deli aplikacije neposredno za enostavnejša opravila.

class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// izračun cene
	}
}

Hrambe: skrbijo za vso komunikacijo s hrambo podatkov, običajno s podatkovno zbirko. Njihova naloga je nalaganje in shranjevanje entitet ter izvajanje metod za njihovo iskanje. Skladišče ščiti preostalo aplikacijo pred podrobnostmi izvajanja podatkovne zbirke in zagotavlja objektno usmerjen vmesnik za delo s podatki.

class OrderRepository
{
	public function find(int $id): ?Order
	{
	}

	public function findByCustomer(int $customerId): array
	{
	}
}

Entitete: objekti, ki predstavljajo glavne poslovne koncepte v aplikaciji, ki imajo svojo identiteto in se s časom spreminjajo. Običajno so to razredi, ki so s pomočjo ORM (kot sta Nette Database Explorer ali Doctrine) preslikani v tabele podatkovne zbirke. Entitete lahko vsebujejo poslovna pravila v zvezi z njihovimi podatki in logiko potrjevanja.

// Entiteta, preslikana v tabelo naročil v zbirki podatkov
class Order extends Nette\Database\Table\ActiveRow
{
	public function addItem(Product $product, int $quantity): void
	{
		$this->related('order_items')->insert([
			'product_id' => $product->id,
			'quantity' => $quantity,
			'unit_price' => $product->price,
		]);
	}
}

Vrednostni objekti: nespremenljivi objekti, ki predstavljajo vrednosti brez lastne identitete – na primer znesek denarja ali e-poštni naslov. Dva primerka objekta vrednosti z enakimi vrednostmi veljata za enaka.

Koda infrastrukture

V mapi Core/ (ali tudi Infrastructure/) se nahaja tehnični temelj aplikacije. Infrastrukturna koda običajno vključuje:

app/Core/
├── Router/               ← usmerjanje in upravljanje URL.
│   └── RouterFactory.php
├── Security/             ← avtentikacija in avtorizacija.
│   ├── Authenticator.php
│   └── Authorizator.php
├── Logging/              ← beleženje in spremljanje
│   ├── SentryLogger.php
│   └── FileLogger.php
├── Cache/                ← plast predpomnilnika
│   └── FullPageCache.php
└── Integration/          ← integracija z zunanjimi storitvami
	├── Slack/
	└── Stripe/

Za manjše projekte seveda zadostuje ravna struktura:

Core/
├── RouterFactory.php
├── Authenticator.php
└── QueueMailer.php

To je koda, ki:

  • Obravnava tehnično infrastrukturo (usmerjanje, beleženje, predpomnilnik).
  • vključuje zunanje storitve (Sentry, Elasticsearch, Redis)
  • zagotavlja osnovne storitve za celotno aplikacijo (pošta, zbirka podatkov)
  • je večinoma neodvisna od določene domene – predpomnilnik ali logger deluje enako za e-trgovino ali blog.

Se sprašujete, ali določen razred spada sem ali v model? Ključna razlika je v tem, da je koda v Core/:

  • ne ve ničesar o domeni (izdelki, naročila, članki)
  • Običajno jo je mogoče prenesti v drug projekt
  • rešuje „kako deluje“ (kako poslati pošto) in ne „kaj počne“ (kakšno pošto poslati)

Primer za boljše razumevanje:

  • App\Core\MailerFactory – ustvari primerke razreda za pošiljanje e-pošte, skrbi za nastavitve SMTP
  • App\Model\OrderMailer – uporablja MailerFactory za pošiljanje e-poštnih sporočil o naročilih, pozna njihove predloge in ve, kdaj jih je treba poslati

Skripte ukazov

Aplikacije morajo pogosto opravljati naloge zunaj običajnih zahtevkov HTTP – bodisi gre za obdelavo podatkov v ozadju, vzdrževanje ali periodična opravila. Za izvajanje se uporabljajo preproste skripte v imeniku bin/, medtem ko je dejanska izvedbena logika nameščena v imeniku app/Tasks/ (ali app/Commands/).

Primer:

app/Tasks/
├── Maintenance/               ← skripte za vzdrževanje
│   ├── CleanupCommand.php     ← brisanje starih podatkov
│   └── DbOptimizeCommand.php  ← optimizacija podatkovne zbirke
├── Integration/               ← integracija z zunanjimi sistemi
│   ├── ImportProducts.php     ← uvoz iz sistema dobavitelja
│   └── SyncOrders.php         ← sinhronizacija naročil
└── Scheduled/                 ← redna opravila
	├── NewsletterCommand.php  ← pošiljanje novic
	└── ReminderCommand.php    ← obveščanje strank

Kaj spada v model in kaj v ukazne skripte? Na primer, logika za pošiljanje enega e-poštnega sporočila je del modela, množično pošiljanje tisočih e-poštnih sporočil pa spada v Tasks/.

Opravila se običajno izvajajo iz ukazne vrstice ali prek programa cron. Lahko se zaženejo tudi prek zahteve HTTP, vendar je pri tem treba upoštevati varnost. Predstavnik, ki izvaja opravilo, mora biti zavarovan, na primer samo za prijavljene uporabnike ali z močnim žetonom in dostopom z dovoljenih naslovov IP. Za dolga opravila je treba povečati časovno omejitev skripta in uporabiti spletno stran session_write_close(), da se prepreči zaklepanje seje.

Drugi možni imeniki

Poleg omenjenih osnovnih imenikov lahko glede na potrebe projekta dodate tudi druge specializirane mape. Oglejmo si najpogostejše in njihovo uporabo:

app/
├── Api/              ← Logika API je neodvisna od predstavitvene plasti
├── Database/         ← migracijske skripte in sejalnike za testne podatke.
├── Components/       ← skupne vizualne komponente v aplikaciji
├── Event/            ← uporabno, če uporabljate arhitekturo, ki temelji na dogodkih
├── Mail/             ← e-poštne predloge in povezana logika
└── Utils/            ← pomožni razredi

Za skupne vizualne komponente, ki se uporabljajo v predstavitvah v celotni aplikaciji, lahko uporabite mapo app/Components ali app/Controls:

app/Components/
├── Form/                 ← komponente obrazca v skupni rabi
│   ├── SignInForm.php
│   └── UserForm.php
├── Grid/                 ← komponente za izpise podatkov
│   └── DataGrid.php
└── Navigation/           ← navigacijski elementi
	├── Breadcrumbs.php
	└── Menu.php

V to mapo spadajo komponente z bolj zapleteno logiko. Če želite komponente deliti med več projekti, jih je dobro ločiti v samostojni paket Composer.

V imenik app/Mail lahko umestite upravljanje komunikacije z elektronsko pošto:

app/Mail/
├── templates/            ← e-poštne predloge
│   ├── order-confirmation.latte
│   └── welcome.latte
└── OrderMailer.php

Predavatelj Mapiranje

Mapiranje določa pravila za izpeljavo imen razredov iz imen predvajalnikov. Določimo jih v konfiguraciji pod ključem application › mapping.

Na tej strani smo pokazali, da predstavnike namestimo v mapo app/Presentation (ali app/UI). V konfiguracijski datoteki moramo Nette obvestiti o tej konvenciji. Zadostuje ena vrstica:

application:
	mapping: App\Presentation\*\**Presenter

Kako deluje preslikava? Za boljše razumevanje si najprej predstavljajmo aplikacijo brez modulov. Želimo, da razredi predstavnikov spadajo v imenski prostor App\Presentation, tako da se predstavnik Home preslika v razred App\Presentation\HomePresenter. To dosežemo s to konfiguracijo:

application:
	mapping: App\Presentation\*Presenter

Mapiranje poteka tako, da se zvezdica v maski App\Presentation\*Presenter zamenja z imenom predstavnika Home, s čimer dobimo končno ime razreda App\Presentation\HomePresenter. Enostavno!

Vendar, kot vidite v primerih v tem in drugih poglavjih, umeščamo predstavitvene razrede v istoimenske podimenike, na primer predstavitveni razred Home se preslika v razred App\Presentation\Home\HomePresenter. To dosežemo s podvojitvijo dvopičja (zahteva aplikacijo Nette 3.2):

application:
	mapping: App\Presentation\**Presenter

Zdaj bomo prešli na preslikavo predstavnikov v module. Za vsak modul lahko določimo posebno kartiranje:

application:
	mapping:
		Front: App\Presentation\Front\**Presenter
		Admin: App\Presentation\Admin\**Presenter
		Api: App\Api\*Presenter

V skladu s to konfiguracijo je predstavnik Front:Home preslikan v razred App\Presentation\Front\Home\HomePresenter, medtem ko je predstavnik Api:OAuth preslikan v razred App\Api\OAuthPresenter.

Ker imata modula Front in Admin podoben način preslikave in ker bo takih modulov verjetno več, je mogoče ustvariti splošno pravilo, ki jih bo nadomestilo. V masko razreda bo dodana nova zvezdica za modul:

application:
	mapping:
		*: App\Presentation\*\**Presenter
		Api: App\Api\*Presenter

Deluje tudi za globlje ugnezdene imeniške strukture, kot je predstavnik Admin:User:Edit, kjer se segment z zvezdico ponovi za vsako raven in ima za rezultat razred App\Presentation\Admin\User\Edit\EditPresenter.

Alternativni zapis je, da namesto niza uporabimo polje, sestavljeno iz treh segmentov. Ta zapis je enakovreden prejšnjemu:

application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]