diff --git a/.gitattributes b/.gitattributes index 9670e954e..ed8103553 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,10 @@ -.gitattributes export-ignore -.gitignore export-ignore -.github export-ignore -ncs.* export-ignore -phpstan.neon export-ignore -tests/ export-ignore +.gitattributes export-ignore +.github/ export-ignore +.gitignore export-ignore +CLAUDE.md export-ignore +ncs.* export-ignore +phpstan*.neon export-ignore +tests/ export-ignore -*.sh eol=lf -*.php* diff=php linguist-language=PHP +*.php* diff=php +*.sh text eol=lf diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 396244b28..be22f1473 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 coverage: none - run: composer install --no-progress --prefer-dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60067d232..b82345087 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1', '8.2', '8.3', '8.4', '8.5'] + php: ['8.2', '8.3', '8.4', '8.5'] fail-fast: false @@ -20,7 +20,7 @@ jobs: coverage: none - run: composer install --no-progress --prefer-dist - - run: vendor/bin/tester tests -s -C + - run: composer tester - if: failure() uses: actions/upload-artifact@v4 with: @@ -35,11 +35,11 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 coverage: none - run: composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable - - run: vendor/bin/tester tests -s -C + - run: composer tester code_coverage: @@ -53,7 +53,7 @@ jobs: coverage: none - run: composer install --no-progress --prefer-dist - - run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src + - run: composer tester -- -p phpdbg --coverage ./coverage.xml --coverage-src ./src - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar - env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index de4a392c3..d49bcd46e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor /composer.lock +tests/lock diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..e7e35a8bc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,578 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Nette Application is a full-stack component-based MVC framework library for PHP. It provides the core application layer for the Nette Framework, handling presenters, components, routing, templating integration, and request/response cycles. + +**Key characteristics:** +- PHP library package (not a full application) +- Supports PHP 8.2 - 8.5 +- Component-based architecture with hierarchical component trees +- Tight integration with Latte templating engine +- Dependency injection through Nette DI bridges + +## Essential Commands + +### Running Tests + +```bash +# Run all tests +vendor/bin/tester tests -s -C + +# Run specific test directory +vendor/bin/tester tests/Application -s -C +vendor/bin/tester tests/UI -s -C + +# Run single test file +php tests/Application/Presenter.twoDomains.phpt +``` + +**Important:** Tests use Nette Tester with `.phpt` extension (148 test files total). + +### Code Quality + +```bash +# Run PHPStan static analysis (level 5) +composer phpstan +``` + +## Architecture + +### Source Structure + +``` +src/ +├── Application/ Main application layer +│ ├── UI/ Presenter & component system +│ │ ├── Presenter.php Base presenter class +│ │ ├── Component.php Base component class +│ │ ├── Control.php Renderable component +│ │ └── Form.php Form component integration +│ ├── Routers/ Routing implementations +│ │ ├── Route.php Standard route +│ │ ├── RouteList.php Route collection +│ │ ├── SimpleRouter.php +│ │ └── CliRouter.php CLI routing +│ ├── Responses/ Response types +│ │ ├── JsonResponse.php +│ │ ├── TextResponse.php +│ │ ├── RedirectResponse.php +│ │ ├── ForwardResponse.php +│ │ └── FileResponse.php +│ ├── Application.php Front controller +│ ├── Request.php Application request +│ ├── LinkGenerator.php URL generation +│ └── PresenterFactory.php +└── Bridges/ Framework integrations + ├── ApplicationDI/ DI container integration + │ ├── ApplicationExtension.php + │ ├── LatteExtension.php + │ └── RoutingExtension.php + ├── ApplicationLatte/ Latte template engine integration + │ ├── TemplateFactory.php + │ ├── TemplateGenerator.php Auto-generates template classes + │ ├── UIExtension.php + │ └── Nodes/ Latte syntax nodes + └── ApplicationTracy/ Tracy debugger integration +``` + +### Key Architectural Concepts + +#### Component Hierarchy + +The framework uses hierarchical component trees where: +- **Presenter** is the root component (extends `Control`, implements `IPresenter`) +- **Control** components can contain child components +- **Component** base class provides parameter persistence and signal handling +- Components use `lookup()` to find ancestors (e.g., `$this->lookup(Presenter::class)`) + +#### Request-Response Cycle + +1. `Application` receives HTTP request via router +2. `PresenterFactory` creates presenter instance +3. `Presenter::run()` processes the request: + - Calls lifecycle methods (`startup()`, `beforeRender()`, `render*()`, `shutdown()`) + - Handles signals (AJAX/component interactions) + - Resolves action/view + - Creates template and renders response +4. Returns `Response` object (Text/Json/Redirect/Forward/File/Void) + +#### Template Integration + +**TemplateGenerator** (new in v3.3): +- Automatically generates typed template classes from presenters/controls +- Creates `{PresenterName}Template` classes with proper property types +- Updates presenter phpDoc with `@property-read` annotations +- Enables full IDE support in `.latte` files via `{templateType}` declaration + +**Bridge Pattern:** +- `Bridges\ApplicationLatte` provides Latte integration +- `Bridges\ApplicationDI` provides DI container extensions +- `Bridges\ApplicationTracy` provides debugging integration + +#### Link Generation + +- `link()` and `n:href` generate URLs to presenter actions/signals +- `LinkGenerator` handles absolute URL generation +- Invalid link handling modes: Silent/Warning/Exception/Textual +- Special syntax: `this`, `//absolute`, `:Module:Presenter:action` + +### Test Structure + +Tests mirror source structure: +``` +tests/ +├── Application/ Application class tests +├── Bridges.DI/ DI extension tests +├── Bridges.Latte/ Latte integration tests (snippets, templates) +├── Bridges.Latte3/ Latte 3.x specific tests +├── Routers/ Router tests +├── Responses/ Response type tests +├── UI/ Presenter & component tests +└── bootstrap.php Test environment setup +``` + +**Test conventions:** +- Use `.phpt` extension (Nette Tester format) +- Use `test()` function for test cases with descriptive names +- Use `testException()` for exception-only tests +- Use `Assert::*` for assertions +- Mockery for mocking dependencies +- Temporary files go to `tests/tmp/{pid}/` + +## Development Guidelines + +### Coding Standards + +- Every PHP file must start with `declare(strict_types=1);` +- Follow Nette Coding Standard (based on PSR-12) +- Use tabs for indentation +- Return type declarations on separate line from closing brace: + ```php + public function example(string $param): + { + // method body + } + ``` + +### Documentation Style + +- Use phpDoc for public APIs +- Document `@property-read` for magic properties +- Document array types: `@return string[]` +- Mark deprecated features with `@deprecated` and explanation +- Interfaces use marker comments for method purposes + +### Presenter Lifecycle + +Presenters follow a strict lifecycle when processing requests. Methods are called sequentially, all optional: + +``` +__construct() + ↓ +startup() ← Always call parent::startup() + ↓ +action() ← Called BEFORE render, can change view/redirect + ↓ +handle() ← Processes signals (AJAX requests, component interactions) + ↓ +beforeRender() ← Common template setup + ↓ +render() ← Prepare data for template + ↓ +afterRender() ← Rarely used + ↓ +shutdown() ← Cleanup +``` + +**Important notes:** +- `action()` executes before `render()` - can change which template renders +- Parameters from URL are automatically passed and type-checked +- Missing/invalid parameters trigger 404 error +- Calling `redirect()`, `error()`, `sendJson()` etc. terminates lifecycle immediately (throws `AbortException`) +- If no response method called, presenter automatically renders template + +### Persistent Parameters + +Persistent parameters maintain state across requests automatically via URL. + +**Declaration:** +```php +use Nette\Application\Attributes\Persistent; + +class ProductPresenter extends Nette\Application\UI\Presenter +{ + #[Persistent] + public string $lang = 'en'; // Must be public, specify type +} +``` + +**How they work:** +- Value automatically included in all generated links +- Transferred across different actions of same presenter +- Can be transferred across presenters if defined in common ancestor/trait +- Changed via `n:href="Product:show lang: cs"` or reset via `lang: null` + +**Validation:** +Override `loadState()` to validate values from URL: +```php +public function loadState(array $params): void +{ + parent::loadState($params); // Sets $this->lang + if (!in_array($this->lang, ['en', 'cs'])) { + $this->error(); // 404 if invalid + } +} +``` + +**Never trust URL parameters** - always validate as users can modify them. + +### Signals & Hollywood Style + +Signals handle user interactions on the current page (sorting, AJAX updates, form submissions). + +**Hollywood Style Philosophy:** +Instead of asking "was button clicked?", tell framework "when button clicked, call this method". Framework calls you back - "Don't call us, we'll call you." + +**Signal Declaration:** +```php +public function handleClick(int $x, int $y): void +{ + // Process the signal + $this->redrawControl(); // Mark for AJAX re-render +} +``` + +**Signal URLs:** +- Created with exclamation mark: `n:href="click! $x, $y"` +- Always called on current presenter/action +- Cannot signal to different presenter +- Format: `?do={signal}` or `?do={component}-{signal}` for component signals + +**In components**, `link()` and `n:href` default to signals (no `!` needed): +```php +// In component template +refresh // Calls handleRefresh() signal +``` + +### Component Factory Pattern + +Components are created lazily via factory methods in presenters. + +**Basic Factory:** +```php +protected function createComponentPoll(): PollControl +{ + return new PollControl; +} +``` + +**Accessing components:** +```php +$poll = $this->getComponent('poll'); // or $this['poll'] +``` + +**Factory is called automatically:** +- First time component is accessed +- Only if actually needed +- Not called during AJAX if component not used + +**Components with Dependencies:** +Use generated factory interface pattern: +```php +// Define interface +interface PollControlFactory +{ + public function create(int $pollId): PollControl; +} + +// Register in config/services.neon +// Nette DI automatically implements interface + +// Use in presenter +class PollPresenter extends Nette\Application\UI\Presenter +{ + public function __construct( + private PollControlFactory $pollControlFactory, + ) { + } + + protected function createComponentPollControl(): PollControl + { + return $this->pollControlFactory->create($pollId: 1); + } +} +``` + +**Multiplier for dynamic components:** +```php +protected function createComponentShopForm(): Multiplier +{ + return new Multiplier(function (string $itemId) { + $form = new Nette\Application\UI\Form; + // ... configure form for $itemId + return $form; + }); +} + +// In template: {control "shopForm-$item->id"} +``` + +### Bidirectional Routing + +Router converts URLs ↔ Presenter:action pairs in both directions. + +**Key concept:** URLs are never hardcoded. Change entire URL structure by modifying router only. + +**Route definition:** +```php +$router = new RouteList; +$router->addRoute('rss.xml', 'Feed:rss'); +$router->addRoute('article/', 'Article:view'); +$router->addRoute('/[/]', 'Home:default'); +``` + +**Route order is critical:** +- Evaluated top to bottom for both matching and generating +- List specific routes before general routes +- First matching route wins + +**Route features:** +- Parameters: ``, `` (with regex validation) +- Optional sequences: `[/]name` +- Default values: `` +- Filters & translations: Czech URLs like `/produkt` → `Product` presenter +- Wildcards: `%domain%`, `%basePath%` +- Modules: `->withModule('Admin')` +- Subdomains: `->withDomain('admin.example.com')` + +**Canonization:** +Framework prevents duplicate content by redirecting alternative URLs to canonical URL (first matching route). Automatic 301 redirect for SEO. + +### AJAX & Snippets + +Snippets update page parts without full reload. + +**Workflow:** +1. Mark element as snippet in template: `{snippet header}...{/snippet}` +2. Invalidate snippet in signal handler: `$this->redrawControl('header')` +3. Nette renders only changed snippets, sends as JSON +4. Naja.js library updates DOM automatically + +**Template syntax:** +```latte +{snippet header} +

Hello {$user->name}

+{/snippet} + +{* Or using n:snippet attribute *} +
+

Hello {$user->name}

+
+``` + +**Signal handler:** +```php +public function handleLogin(string $user): void +{ + $this->user = $user; + $this->redrawControl('header'); // Invalidate specific snippet + // or $this->redrawControl() to invalidate all snippets +} +``` + +**Dynamic snippets with snippetArea:** +```latte +
    + {foreach $items as $id => $item} +
  • {$item}
  • + {/foreach} +
+``` + +```php +$this->redrawControl('itemsContainer'); // Must invalidate parent area +$this->redrawControl('item-1'); // And specific snippet +``` + +**Client-side (Naja.js):** +```html + +Click me +
...
+``` + +**Sending custom data:** +```php +public function handleDelete(int $id): void +{ + // ... + if ($this->isAjax()) { + $this->payload->message = 'Deleted successfully'; + } +} +``` + +### Template Lookup + +Framework automatically finds templates - no need to specify paths. + +**Action template lookup:** +``` +Presentation/Home/HomePresenter.php +Presentation/Home/default.latte ← Found automatically +``` + +**Alternative structure:** +``` +Presenters/HomePresenter.php +Presenters/templates/Home.default.latte ← or +Presenters/templates/Home/default.latte ← or +``` + +**Layout template lookup:** +``` +Presentation/@layout.latte ← Common for all +Presentation/Home/@layout.latte ← Specific for Home +``` + +**Override in code:** +```php +$this->setView('otherView'); // Change view +$this->template->setFile('/path/to/file.latte'); // Explicit path +$this->setLayout('layoutAdmin'); // Different layout +$this->setLayout(false); // No layout +``` + +**Type-safe templates:** +```php +/** + * @property-read ArticleTemplate $template + */ +class ArticlePresenter extends Nette\Application\UI\Presenter +{ +} + +class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template +{ + public Article $article; + public User $user; +} +``` + +In template: +```latte +{templateType App\Presentation\Article\ArticleTemplate} +{* Now full IDE autocomplete for $article, $user *} +``` + +### Link Syntax + +Links to presenters/actions use special syntax instead of URLs. + +**In templates:** +```latte +detail +detail in EN +refresh current page +current page with changed param + +{* Absolute URL *} +absolute + +{* Module navigation *} +absolute to module + +{* Signals (notice the !) *} +signal + +{* Fragment *} +jump to #main +``` + +**In presenter code:** +```php +$url = $this->link('Product:show', $id); +$url = $this->link('Product:show', [$id, 'lang' => 'en']); + +// Redirects +$this->redirect('Product:show', $id); // 302/303 +$this->redirectPermanent('Product:show', $id); // 301 +$this->redirectUrl('https://example.com'); +$this->forward('Product:show'); // No HTTP redirect +``` + +**Link checking:** +```latte +{if isLinkCurrent('Product:show')}active{/if} +
  • ...
  • +``` + +**Invalid links:** +Configured via `Presenter::$invalidLinkMode`: +- `InvalidLinkSilent` - returns `#` +- `InvalidLinkWarning` - logs E_USER_WARNING (production default) +- `InvalidLinkTextual` - shows error in link text (dev default) +- `InvalidLinkException` - throws exception + +### Flash Messages + +Flash messages survive redirects and stay for 30 seconds (for page refresh tolerance). + +```php +public function handleDelete(int $id): void +{ + // ... delete item + $this->flashMessage('Item deleted successfully.'); + $this->redirect('this'); +} +``` + +In template: +```latte +{foreach $flashes as $flash} +
    {$flash->message}
    +{/foreach} +``` + +With type: +```php +$this->flashMessage('Error occurred', 'error'); +$this->flashMessage('Success', 'success'); +``` + +### Application Configuration + +Key configuration options in `config/common.neon`: + +```neon +application: + errorPresenter: Error # 4xx and 5xx errors + # Or separate error presenters: + errorPresenter: + 4xx: Error4xx + 5xx: Error5xx + + silentLinks: false # Suppress invalid link warnings in dev + + mapping: # Presenter name → class mapping + *: App\*Module\Presentation\*Presenter + + aliases: # Short aliases for links + home: Front:Home:default + admin: Admin:Dashboard:default + +latte: + strictTypes: false # Add declare(strict_types=1) to templates + strictParsing: false # Strict parser mode + locale: cs_CZ # Locale for filters +``` + +### Breaking Changes + +This is v3.3 branch. Recent BC breaks include: +- `Application::processRequest()` now returns `Response` (not void) +- `@annotations` deprecated in favor of PHP 8 attributes +- Various deprecation notices for old APIs diff --git a/composer.json b/composer.json index 68fb11e72..51ec77a45 100644 --- a/composer.json +++ b/composer.json @@ -15,11 +15,11 @@ } ], "require": { - "php": "8.1 - 8.5", - "nette/component-model": "^3.1", + "php": "8.2 - 8.5", + "nette/component-model": "^3.2", "nette/http": "^3.3.2", "nette/routing": "^3.1.1", - "nette/utils": "^4.0" + "nette/utils": "^4.1" }, "suggest": { "nette/forms": "Allows to use Nette\\Application\\UI\\Form", @@ -31,8 +31,8 @@ "nette/forms": "^3.2", "nette/robot-loader": "^4.0", "nette/security": "^3.2", - "latte/latte": "^2.10.2 || ^3.0.18", - "tracy/tracy": "^2.9", + "latte/latte": "^3.1", + "tracy/tracy": "^2.11", "mockery/mockery": "^1.6@stable", "phpstan/phpstan-nette": "^2.0@stable", "jetbrains/phpstorm-attributes": "^1.2" @@ -42,8 +42,8 @@ "nette/di": "<3.2", "nette/forms": "<3.2", "nette/schema": "<1.3", - "latte/latte": "<2.7.1 ||>=3.0.0 <3.0.18 ||>=3.2", - "tracy/tracy": "<2.9" + "latte/latte": "<3.1 ||>=3.2", + "tracy/tracy": "<2.11" }, "autoload": { "classmap": ["src/"], @@ -58,7 +58,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } } } diff --git a/readme.md b/readme.md index d8e52ef86..41ddf658c 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ Nette Application MVC ===================== [![Downloads this Month](https://img.shields.io/packagist/dm/nette/application.svg)](https://packagist.org/packages/nette/application) -[![Tests](https://github.com/nette/application/actions/workflows/tests.yml/badge.svg?branch=v3.2)](https://github.com/nette/application/actions) +[![Tests](https://github.com/nette/application/actions/workflows/tests.yml/badge.svg?branch=v3.3)](https://github.com/nette/application/actions) [![Latest Stable Version](https://poser.pugx.org/nette/application/v/stable)](https://github.com/nette/application/releases) [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/application/blob/master/license.md) diff --git a/src/Application/Application.php b/src/Application/Application.php index be96dd49c..858ae0e71 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -48,22 +48,14 @@ class Application /** @var Request[] */ private array $requests = []; private ?IPresenter $presenter = null; - private Nette\Http\IRequest $httpRequest; - private Nette\Http\IResponse $httpResponse; - private IPresenterFactory $presenterFactory; - private Router $router; public function __construct( - IPresenterFactory $presenterFactory, - Router $router, - Nette\Http\IRequest $httpRequest, - Nette\Http\IResponse $httpResponse, + private readonly IPresenterFactory $presenterFactory, + private readonly Router $router, + private readonly Nette\Http\IRequest $httpRequest, + private readonly Nette\Http\IResponse $httpResponse, ) { - $this->httpRequest = $httpRequest; - $this->httpResponse = $httpResponse; - $this->presenterFactory = $presenterFactory; - $this->router = $router; } @@ -74,7 +66,8 @@ public function run(): void { try { Arrays::invoke($this->onStartup, $this); - $this->processRequest($this->createInitialRequest()); + $this->processRequest($this->createInitialRequest()) + ->send($this->httpRequest, $this->httpResponse); Arrays::invoke($this->onShutdown, $this); } catch (\Throwable $e) { @@ -82,7 +75,8 @@ public function run(): void Arrays::invoke($this->onError, $this, $e); if ($this->catchExceptions && ($req = $this->createErrorRequest($e))) { try { - $this->processRequest($req); + $this->processRequest($req) + ->send($this->httpRequest, $this->httpResponse); Arrays::invoke($this->onShutdown, $this, $e); return; @@ -121,7 +115,7 @@ public function createInitialRequest(): Request } - public function processRequest(Request $request): void + public function processRequest(Request $request): Response { process: if (count($this->requests)> $this->maxLoop) { @@ -156,7 +150,7 @@ public function processRequest(Request $request): void } Arrays::invoke($this->onResponse, $this, $response); - $response->send($this->httpRequest, $this->httpResponse); + return $response; } diff --git a/src/Application/Helpers.php b/src/Application/Helpers.php index 51b266cea..ca6a4e869 100644 --- a/src/Application/Helpers.php +++ b/src/Application/Helpers.php @@ -34,7 +34,7 @@ public static function splitName(string $name): array /** - * return string[] + * @return array */ public static function getClassesAndTraits(string $class): array { diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index 122fdfc7b..3ec9709bd 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -245,6 +245,7 @@ public static function parseDestination(string $destination): array } if (!empty($matches['query'])) { + trigger_error("Link format is obsolete, use arguments instead of query string in '$destination'.", E_USER_DEPRECATED); parse_str(substr($matches['query'], 1), $args); } @@ -273,7 +274,7 @@ public function requestToUrl(Request $request, ?bool $relative = false): string if ($relative) { $hostUrl = $this->refUrl->getHostUrl() . '/'; - if (strncmp($url, $hostUrl, strlen($hostUrl)) === 0) { + if (str_starts_with($url, $hostUrl)) { $url = substr($url, strlen($hostUrl) - 1); } } diff --git a/src/Application/MicroPresenter.php b/src/Application/MicroPresenter.php index b68eea59e..f0312f828 100644 --- a/src/Application/MicroPresenter.php +++ b/src/Application/MicroPresenter.php @@ -115,7 +115,7 @@ public function createTemplate(?string $class = null, ?callable $latteFactory = { $latte = $latteFactory ? $latteFactory() - : $this->getContext()->getByType(Nette\Bridges\ApplicationLatte\LatteFactory::class)->create(); + : $this->context->getByType(Nette\Bridges\ApplicationLatte\LatteFactory::class)->create(); $template = $class ? new $class : new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte); @@ -146,7 +146,7 @@ public function redirectUrl(string $url, int $httpCode = Http\IResponse::S302_Fo * Throws HTTP error. * @throws Nette\Application\BadRequestException */ - public function error(string $message = '', int $httpCode = Http\IResponse::S404_NotFound): void + public function error(string $message = '', int $httpCode = Http\IResponse::S404_NotFound): never { throw new Application\BadRequestException($message, $httpCode); } diff --git a/src/Application/PresenterFactory.php b/src/Application/PresenterFactory.php index 063ff2ccb..52fc5e4f3 100644 --- a/src/Application/PresenterFactory.php +++ b/src/Application/PresenterFactory.php @@ -18,17 +18,17 @@ */ class PresenterFactory implements IPresenterFactory { - /** @var array[] of module => splited mask */ + /** @var array module => splited mask */ private array $mapping = [ - '*' => ['', '*Module\\', '*Presenter'], + '*' => ['App\Presentation\\', '*\\', '**Presenter'], 'Nette' => ['NetteModule\\', '*\\', '*Presenter'], ]; private array $aliases = []; private array $cache = []; - /** @var callable */ - private $factory; + /** @var \Closure(string): IPresenter */ + private \Closure $factory; /** @@ -36,7 +36,9 @@ class PresenterFactory implements IPresenterFactory */ public function __construct(?callable $factory = null) { - $this->factory = $factory ?? fn(string $class): IPresenter => new $class; + $this->factory = $factory + ? $factory(...) + : fn(string $class): IPresenter => new $class; } @@ -78,6 +80,7 @@ public function getPresenterClass(string &$name): string /** * Sets mapping as pairs [module => mask] + * @param array $mapping */ public function setMapping(array $mapping): static { @@ -123,6 +126,7 @@ public function formatPresenterClass(string $presenter): string /** * Sets pairs [alias => destination] + * @param array $aliases */ public function setAliases(array $aliases): static { diff --git a/src/Application/Request.php b/src/Application/Request.php index 72f46f181..d988d7d9f 100644 --- a/src/Application/Request.php +++ b/src/Application/Request.php @@ -16,11 +16,11 @@ /** * Presenter request. * - * @property string $presenterName - * @property array $parameters - * @property array $post - * @property array $files - * @property string|null $method + * @property-deprecated string $presenterName + * @property-deprecated array $parameters + * @property-deprecated array $post + * @property-deprecated array $files + * @property-deprecated ?string $method */ final class Request { diff --git a/src/Application/Responses/CallbackResponse.php b/src/Application/Responses/CallbackResponse.php index 91a2cc74c..67647b3a4 100644 --- a/src/Application/Responses/CallbackResponse.php +++ b/src/Application/Responses/CallbackResponse.php @@ -17,16 +17,16 @@ */ final class CallbackResponse implements Nette\Application\Response { - /** @var callable */ - private $callback; + /** @var \Closure(Nette\Http\IRequest, Nette\Http\IResponse): void */ + private \Closure $callback; /** - * @param callable(Nette\Http\IRequest, Nette\Http\Response): void $callback + * @param callable(Nette\Http\IRequest, Nette\Http\IResponse): void $callback */ public function __construct(callable $callback) { - $this->callback = $callback; + $this->callback = $callback(...); } diff --git a/src/Application/Responses/FileResponse.php b/src/Application/Responses/FileResponse.php index f45b74bb8..4c98f3d9d 100644 --- a/src/Application/Responses/FileResponse.php +++ b/src/Application/Responses/FileResponse.php @@ -19,26 +19,20 @@ final class FileResponse implements Nette\Application\Response { public bool $resuming = true; - private string $file; - private string $contentType; - private string $name; - private bool $forceDownload; + private readonly string $name; public function __construct( - string $file, + private readonly string $file, ?string $name = null, - ?string $contentType = null, - bool $forceDownload = true, + private readonly string $contentType = 'application/octet-stream', + private readonly bool $forceDownload = true, ) { if (!is_file($file) || !is_readable($file)) { throw new Nette\Application\BadRequestException("File '$file' doesn't exist or is not readable."); } - $this->file = $file; $this->name = $name ?? basename($file); - $this->contentType = $contentType ?? 'application/octet-stream'; - $this->forceDownload = $forceDownload; } diff --git a/src/Application/Responses/ForwardResponse.php b/src/Application/Responses/ForwardResponse.php index 9e68c16ca..23675669c 100644 --- a/src/Application/Responses/ForwardResponse.php +++ b/src/Application/Responses/ForwardResponse.php @@ -17,12 +17,9 @@ */ final class ForwardResponse implements Nette\Application\Response { - private Nette\Application\Request $request; - - - public function __construct(Nette\Application\Request $request) - { - $this->request = $request; + public function __construct( + private readonly Nette\Application\Request $request, + ) { } diff --git a/src/Application/Responses/JsonResponse.php b/src/Application/Responses/JsonResponse.php index 1feb0d6f4..998435b42 100644 --- a/src/Application/Responses/JsonResponse.php +++ b/src/Application/Responses/JsonResponse.php @@ -17,14 +17,10 @@ */ final class JsonResponse implements Nette\Application\Response { - private mixed $payload; - private string $contentType; - - - public function __construct(mixed $payload, ?string $contentType = null) - { - $this->payload = $payload; - $this->contentType = $contentType ?? 'application/json'; + public function __construct( + private readonly mixed $payload, + private readonly string $contentType = 'application/json', + ) { } diff --git a/src/Application/Responses/RedirectResponse.php b/src/Application/Responses/RedirectResponse.php index 1ab884f1a..9974b0c8c 100644 --- a/src/Application/Responses/RedirectResponse.php +++ b/src/Application/Responses/RedirectResponse.php @@ -18,14 +18,10 @@ */ final class RedirectResponse implements Nette\Application\Response { - private string $url; - private int $httpCode; - - - public function __construct(string $url, int $httpCode = Http\IResponse::S302_Found) - { - $this->url = $url; - $this->httpCode = $httpCode; + public function __construct( + private readonly string $url, + private readonly int $httpCode = Http\IResponse::S302_Found, + ) { } diff --git a/src/Application/Responses/TextResponse.php b/src/Application/Responses/TextResponse.php index e21fb5168..ac486472d 100644 --- a/src/Application/Responses/TextResponse.php +++ b/src/Application/Responses/TextResponse.php @@ -17,12 +17,9 @@ */ final class TextResponse implements Nette\Application\Response { - private mixed $source; - - - public function __construct(mixed $source) - { - $this->source = $source; + public function __construct( + private readonly mixed $source, + ) { } diff --git a/src/Application/Routers/CliRouter.php b/src/Application/Routers/CliRouter.php index 7e261fdf0..c02cfb8e3 100644 --- a/src/Application/Routers/CliRouter.php +++ b/src/Application/Routers/CliRouter.php @@ -20,12 +20,11 @@ final class CliRouter implements Nette\Routing\Router { private const PresenterKey = 'action'; - private array $defaults; - - public function __construct(array $defaults = []) - { - $this->defaults = $defaults; + public function __construct( + /** @var array */ + private readonly array $defaults = [], + ) { } @@ -101,6 +100,7 @@ public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?stri /** * Returns default values. + * @return array */ public function getDefaults(): array { diff --git a/src/Application/Routers/Route.php b/src/Application/Routers/Route.php index fbc85f3bf..a5445f82b 100644 --- a/src/Application/Routers/Route.php +++ b/src/Application/Routers/Route.php @@ -10,7 +10,7 @@ namespace Nette\Application\Routers; use Nette; -use function interface_exists, is_string, lcfirst, preg_replace, rawurlencode, str_replace, strlen, strncmp, strrpos, strtolower, strtr, substr, ucwords; +use function is_string, lcfirst, preg_replace, rawurlencode, str_replace, strlen, strncmp, strrpos, strtolower, strtr, substr, ucwords; /** @@ -87,7 +87,7 @@ public function match(Nette\Http\IRequest $httpRequest): ?array $presenter = $params[self::PresenterKey] ?? null; if (isset($this->getMetadata()[self::ModuleKey], $params[self::ModuleKey]) && is_string($presenter)) { - $params[self::PresenterKey] = $params[self::ModuleKey] . ':' . $params[self::PresenterKey]; + $params[self::PresenterKey] = $params[self::ModuleKey] . ':' . $presenter; } unset($params[self::ModuleKey]); @@ -98,6 +98,7 @@ public function match(Nette\Http\IRequest $httpRequest): ?array /** * Constructs absolute URL from array. + * @param array $params */ public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string { @@ -121,7 +122,10 @@ public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?stri } - /** @internal */ + /** + * @return array + * @internal + */ public function getConstantParameters(): array { $res = parent::getConstantParameters(); @@ -188,6 +192,3 @@ public static function path2presenter(string $s): string return $s; } } - - -interface_exists(Nette\Application\IRouter::class); diff --git a/src/Application/Routers/RouteList.php b/src/Application/Routers/RouteList.php index 463589ee5..e18384c44 100644 --- a/src/Application/Routers/RouteList.php +++ b/src/Application/Routers/RouteList.php @@ -11,7 +11,7 @@ use JetBrains\PhpStorm\Language; use Nette; -use function count, interface_exists, is_int, is_string, strlen, strncmp, substr; +use function count, is_int, is_string, strlen, strncmp, substr; /** @@ -33,6 +33,8 @@ public function __construct(?string $module = null) /** * Support for modules. + * @param array $params + * @return ?array */ protected function completeParameters(array $params): ?array { @@ -47,11 +49,12 @@ protected function completeParameters(array $params): ?array /** * Constructs absolute URL from array. + * @param array $params */ public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string { if ($this->module) { - if (strncmp($params[self::PresenterKey], $this->module, strlen($this->module)) !== 0) { + if (!str_starts_with($params[self::PresenterKey], $this->module)) { return null; } @@ -96,6 +99,13 @@ public function getModule(): ?string */ public function offsetSet($index, $router): void { + if ($router instanceof Route) { + trigger_error('Usage `$router[] = new Route(...)` is deprecated, use `$router->addRoute(...)`.', E_USER_DEPRECATED); + } else { + $class = getclass($router); + trigger_error("Usage `\$router[] = new $class` is deprecated, use `\$router->add(new $class)`.", E_USER_DEPRECATED); + } + if ($index === null) { $this->add($router); } else { @@ -110,6 +120,7 @@ public function offsetSet($index, $router): void */ public function offsetGet($index): Nette\Routing\Router { + trigger_error('Usage `$route = $router[...]` is deprecated, use `$router->getRouters()`.', E_USER_DEPRECATED); if (!$this->offsetExists($index)) { throw new Nette\OutOfRangeException('Offset invalid or out of range'); } @@ -123,6 +134,7 @@ public function offsetGet($index): Nette\Routing\Router */ public function offsetExists($index): bool { + trigger_error('Usage `isset($router[...])` is deprecated.', E_USER_DEPRECATED); return is_int($index) && $index>= 0 && $index < count($this->getRouters()); } @@ -133,6 +145,7 @@ public function offsetExists($index): bool */ public function offsetUnset($index): void { + trigger_error('Usage `unset($router[$index])` is deprecated, use `$router->modify($index, null)`.', E_USER_DEPRECATED); if (!$this->offsetExists($index)) { throw new Nette\OutOfRangeException('Offset invalid or out of range'); } @@ -140,6 +153,3 @@ public function offsetUnset($index): void $this->modify($index, null); } } - - -interface_exists(Nette\Application\IRouter::class); diff --git a/src/Application/Routers/SimpleRouter.php b/src/Application/Routers/SimpleRouter.php index ac77ea453..c8e8c49b3 100644 --- a/src/Application/Routers/SimpleRouter.php +++ b/src/Application/Routers/SimpleRouter.php @@ -39,6 +39,3 @@ public function __construct(array|string $defaults = []) parent::__construct($defaults); } } - - -interface_exists(Nette\Application\IRouter::class); diff --git a/src/Application/UI/AccessPolicy.php b/src/Application/UI/AccessPolicy.php index 4c4ae29b4..b478d3ed6 100644 --- a/src/Application/UI/AccessPolicy.php +++ b/src/Application/UI/AccessPolicy.php @@ -62,6 +62,10 @@ private function getAttributes(): array } + /** + * @param Attributes\Requires[] $attrs + * @return Attributes\Requires[] + */ private function applyInternalRules(array $attrs, Component $component): array { if ( diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index e4feedfe2..383b84f98 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -10,7 +10,7 @@ namespace Nette\Application\UI; use Nette; -use function array_key_exists, array_slice, class_exists, func_get_arg, func_get_args, func_num_args, get_debug_type, is_array, link, method_exists, sprintf, trigger_error; +use function array_key_exists, array_slice, func_get_arg, func_get_args, func_num_args, get_debug_type, is_array, link, method_exists, sprintf, trigger_error; /** @@ -20,8 +20,8 @@ * other child components, and interact with user. Components have properties * for storing their status, and responds to user command. * - * @property-read Presenter $presenter - * @property-read bool $linkCurrent + * @property-deprecated Presenter $presenter + * @property-deprecated bool $linkCurrent */ abstract class Component extends Nette\ComponentModel\Container implements SignalReceiver, StatePersistent, \ArrayAccess { @@ -29,20 +29,17 @@ abstract class Component extends Nette\ComponentModel\Container implements Signa /** @var array Occurs when component is attached to presenter */ public array $onAnchor = []; + + /** @var array */ protected array $params = []; /** * Returns the presenter where this component belongs to. */ - public function getPresenter(): ?Presenter + public function getPresenter(): Presenter { - if (func_num_args()) { - trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getPresenterIfExists()', E_USER_DEPRECATED); - $throw = func_get_arg(0); - } - - return $this->lookup(Presenter::class, throw: $throw ?? true); + return $this->lookup(Presenter::class); } @@ -99,10 +96,11 @@ protected function validateParent(Nette\ComponentModel\IContainer $parent): void /** * Calls public method if exists. + * @param array $params */ protected function tryCall(string $method, array $params): bool { - $rc = $this->getReflection(); + $rc = static::getReflection(); if (!$rc->hasMethod($method)) { return false; } elseif (!$rc->hasCallableMethod($method)) { @@ -146,10 +144,11 @@ public static function getReflection(): ComponentReflection /** * Loads state information. + * @param array $params */ public function loadState(array $params): void { - $reflection = $this->getReflection(); + $reflection = static::getReflection(); foreach ($reflection->getParameters() as $name => $meta) { if (isset($params[$name])) { // nulls are ignored if (!ParameterConverter::convertType($params[$name], $meta['type'])) { @@ -174,6 +173,7 @@ public function loadState(array $params): void /** * Saves state information for next request. + * @param array $params */ public function saveState(array &$params): void { @@ -183,6 +183,7 @@ public function saveState(array &$params): void /** * @internal used by presenter + * @param array $params */ public function saveStatePartial(array &$params, ComponentReflection $reflection): void { @@ -226,6 +227,7 @@ public function saveStatePartial(array &$params, ComponentReflection $reflection final public function getParameter(string $name): mixed { if (func_num_args()> 1) { + trigger_error(__METHOD__ . '() parameter $default is deprecated, use operator ??', E_USER_DEPRECATED); $default = func_get_arg(1); } return $this->params[$name] ?? $default ?? null; @@ -234,6 +236,7 @@ final public function getParameter(string $name): mixed /** * Returns component parameters. + * @return array */ final public function getParameters(): array { @@ -260,7 +263,7 @@ final public function getParameterId(string $name): string */ public function signalReceived(string $signal): void { - if (!$this->tryCall($this->formatSignalMethod($signal), $this->params)) { + if (!$this->tryCall(static::formatSignalMethod($signal), $this->params)) { $class = static::class; throw new BadSignalException("There is no handler for signal '$signal' in class $class."); } @@ -379,6 +382,3 @@ public function error(string $message = '', int $httpCode = Nette\Http\IResponse throw new Nette\Application\BadRequestException($message, $httpCode); } } - - -class_exists(PresenterComponent::class); diff --git a/src/Application/UI/ComponentReflection.php b/src/Application/UI/ComponentReflection.php index e8454ff4e..3af1d6833 100644 --- a/src/Application/UI/ComponentReflection.php +++ b/src/Application/UI/ComponentReflection.php @@ -11,7 +11,7 @@ use Nette\Application\Attributes; use Nette\Utils\Reflection; -use function array_fill_keys, array_filter, array_key_exists, array_merge, class_exists, end, preg_match_all, preg_quote, preg_split, strtolower; +use function array_fill_keys, array_filter, array_key_exists, array_merge, end, preg_match_all, preg_quote, preg_split, strtolower; use const PREG_SPLIT_NO_EMPTY; @@ -29,8 +29,8 @@ final class ComponentReflection extends \ReflectionClass /** - * Returns array of class properties that are public and have attribute #[Persistent] or #[Parameter] or annotation @persistent. - * @return array + * Returns array of class properties that are public and have attribute #[Persistent] or #[Parameter]. + * @return array */ public function getParameters(): array { @@ -77,8 +77,8 @@ public function getParameters(): array /** - * Returns array of persistent properties. They are public and have attribute #[Persistent] or annotation @persistent. - * @return array + * Returns array of persistent properties. They are public and have attribute #[Persistent]. + * @return array */ public function getPersistentParams(): array { @@ -115,6 +115,9 @@ public function getPersistentComponents(): array } + /** + * @return string[] names of public properties with #[TemplateVariable] attribute + */ public function getTemplateVariables(Control $control): array { $res = []; @@ -169,6 +172,7 @@ public function getSignalMethod(string $signal): ?\ReflectionMethod /** * Returns an annotation value. + * @deprecated */ public static function parseAnnotation(\Reflector $ref, string $name): ?array { @@ -186,22 +190,25 @@ public static function parseAnnotation(\Reflector $ref, string $name): ?array } } + $alt = match ($name) { + 'persistent' => '#[Nette\Application\Attributes\Persistent]', + 'deprecated' => '#[Nette\Application\Attributes\Deprecated]', + 'crossOrigin' => '#[Nette\Application\Attributes\Request(sameOrigin: false)]', + default => 'alternative' + }; + trigger_error("Annotation @$name is deprecated, use $alt (used in " . Reflection::toString($ref) . ')', E_USER_DEPRECATED); return $res; } - /** - * Has class specified annotation? - */ + #[\Deprecated] public function hasAnnotation(string $name): bool { return (bool) self::parseAnnotation($this, $name); } - /** - * Returns an annotation value. - */ + #[\Deprecated] public function getAnnotation(string $name): mixed { $res = self::parseAnnotation($this, $name); @@ -228,12 +235,9 @@ public function getMethods($filter = -1): array } - /** @deprecated */ + #[\Deprecated] public static function combineArgs(\ReflectionFunctionAbstract $method, array $args): array { return ParameterConverter::toArguments($method, $args); } } - - -class_exists(PresenterComponentReflection::class); diff --git a/src/Application/UI/Control.php b/src/Application/UI/Control.php index 24484dc3d..0bf7bc2cd 100644 --- a/src/Application/UI/Control.php +++ b/src/Application/UI/Control.php @@ -46,6 +46,11 @@ final public function getTemplate(): Template } + /** + * @template T of Template + * @param ?class-string $class + * @return ($class is null ? Template : T) + */ protected function createTemplate(?string $class = null): Template { $class ??= $this->formatTemplateClass(); @@ -54,13 +59,17 @@ protected function createTemplate(?string $class = null): Template } + /** @return ?class-string