Invoke before designing presenters, modules, or application structure in web applicartion.
For new projects, see the project skeleton reference.
For the #[Requires] attribute (HTTP method/AJAX restrictions on actions), see the reference.
Understanding the request flow is essential for placing logic correctly:
checkRequirements() ā evaluates the #[Requires] attributestartup() ā use for access checks and early redirects. An override must call
parent::startup(), otherwise Nette throws InvalidStateExceptionaction<Name>() ā processes the request (data writes, redirects)handle<Signal>() ā signals run after the action phase and after canonicalization,
not inside the action phasebeforeRender() ā runs before every render, use for shared template variablesrender<Name>() ā prepares data for the template (read-only, no redirects)afterRender() ā after the render methods, before the template is sentshutdown()Each phase also has an event: $onStartup, $onRender, $onShutdown.
The key insight: actions and signals do things (write, redirect), renders prepare views (read). Mixing these responsibilities leads to redirect-after-render bugs and untestable presenters.
The application follows domain-driven organization. The reason: when code is grouped by domain (products, orders, customers), related files are close together and changes to one feature don't scatter across multiple directories.
App\)Start minimal -> Grow organically -> Refactor when painful
Start with flat structure ā create subdirectories only when you have 5+ related files or clear implementation variants. The threshold exists because below 5 files, subdirectories add navigation overhead without improving discoverability.
Don't architect for theoretical future complexity. Address actual complexity when it emerges with clear user needs driving structural decisions.
When refactoring to deeper structure: Move files one domain at a time. Create the new subdirectory, move related presenters/services into it, update namespaces, and verify. Don't reorganize everything at once.
For DI configuration details (service registration, autowiring, parameters), see the nette-configuration skill.
The distinction matters because Core/ code is reusable across projects while Model/ code is specific to your business. This affects testability, replaceability, and team ownership.
Use Core/ for:
Use Model/ for:
app/Model/
āāā CatalogService.php ā Main domain services at root
āāā CustomerService.php
āāā OrderService.php
āāā mails/ ā Email templates (specialized assets)
āāā Payment/ ā Implementation variants
ā āāā CardOnlinePayment.php
ā āāā BankTransferPayment.php
ā āāā CashPayment.php
āāā exceptions.php ā Domain exceptions
Naming convention: mails/ is lowercase because it contains non-PHP assets (email templates). Payment/ is uppercase because it contains PHP classes following PSR-4.
Service placement rules:
Modules group presenters by user audience and access requirements. Admin, Front, and Api have different authentication, layouts, and URL patterns ā that's why they're separate modules, not just for organization.
app/Presentation/
āāā Accessory/ ā UI shared across entire application
ā āāā LatteExtension.php
ā āāā TemplateFilters.php
āāā Admin/
ā āāā BasePresenter.php ā Admin-specific functionality
ā āāā Auth/ ā Authentication
ā āāā Catalog/ ā Product management
ā ā āāā Brand/
ā ā āāā List/ ā Overview/utility presenters
ā ā āāā Product/
ā āāā Fulfill/ ā Order processing
āāā Front/
āāā Customer/
āāā Listing/
Keep presenters flat until complexity demands structure:
# Start simple
Dashboard/DashboardPresenter.php
# Grow when needed
Admin/Catalog/Product/ProductPresenter.php
Admin/Catalog/Brand/BrandPresenter.php
Admin/Catalog/List/ListPresenter.php
Create nested structure when:
Each presenter directory contains the presenter class, its templates, and its local components:
Product/
āāā ProductPresenter.php
āāā default.latte
āāā edit.latte
āāā ProductFormFactory.php ā Form factory used only by this presenter
For template organization details (layouts, partials, @-prefixed files), see the latte-templates skill.
Create BasePresenter for each major module only when needed:
Admin\BasePresenter ā authentication checks, admin-specific setupstartup() checks, beforeRender() template variablesAvoid deep inheritance ā prefer composition over inheritance chains deeper than BasePresenter -> SpecificPresenter. Deep chains make it hard to understand which method runs when and create fragile coupling between unrelated presenters.
Where to place components, form factories, Latte extensions, and other shared code follows a proximity principle ā keep code close to where it's used:
In the presenter directory ā used by one presenter only:
Product/
āāā ProductPresenter.php
āāā ProductFormFactory.php ā Only ProductPresenter uses this
āāā edit.latte
In Module/Accessory/ ā shared across presenters within one module:
Admin/
āāā Accessory/
ā āāā DataGridFactory.php ā Used by multiple Admin presenters
ā āāā AdminFilters.php ā Admin-specific template helpers
āāā Product/
āāā Order/
In Presentation/Accessory/ ā shared across modules:
Presentation/
āāā Accessory/
ā āāā LatteExtension.php ā App-wide Latte filters/functions
ā āāā NavigationFactory.php ā Used in Admin and Front
ā āāā TemplateFilters.php
Form factories that encapsulate form creation with validation and callbacks are preferred over building forms directly in presenters when the same form appears in multiple places. For form factory implementation patterns, see the nette-forms skill.
Create module when:
Avoid modules for:
app/Tasks/
āāā Maintenance/ ā Cleanup, optimization
āāā Integration/ ā External data sync
āāā Scheduled/ ā Recurring operations
Task responsibility boundaries:
This separation means business logic is testable without CLI context and reusable from presenters or other entry points.
Don't separate by technical layer ā Services/, Repositories/, Controllers/ separation forces you to jump between directories for every feature change. Domain organization keeps related code together.
Don't create deep hierarchies ā prefer descriptive names over nested structure (OrderFulfillmentService vs Fulfill/Order/Service). Deep nesting increases cognitive load and makes imports longer without adding clarity.
For details, see the official documentation: