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..a0135a8d8 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": "4.0-dev" } } } diff --git a/readme.md b/readme.md index d8e52ef86..b4b61840e 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=master)](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..efc3e6d88 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); } } @@ -301,9 +302,7 @@ private function validateLinkTarget( { if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->isLinkable()) { throw new UI\InvalidLinkException("Link to forbidden $message from '{$presenter->getName()}:{$presenter->getAction()}'."); - } elseif ($presenter?->invalidLinkMode - && (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class)) - ) { + } elseif ($presenter?->invalidLinkMode && $element->getAttributes(Attributes\Deprecated::class)) { trigger_error("Link to deprecated $message from '{$presenter->getName()}:{$presenter->getAction()}'.", E_USER_DEPRECATED); } } 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..68972780a 100644 --- a/src/Application/PresenterFactory.php +++ b/src/Application/PresenterFactory.php @@ -20,7 +20,7 @@ class PresenterFactory implements IPresenterFactory { /** @var array[] of module => splited mask */ private array $mapping = [ - '*' => ['', '*Module\\', '*Presenter'], + '*' => ['App\Presentation\\', '*\\', '**Presenter'], 'Nette' => ['NetteModule\\', '*\\', '*Presenter'], ]; diff --git a/src/Application/Request.php b/src/Application/Request.php index 72f46f181..3640bfb81 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|null $method */ final class Request { 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..530f965b0 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; /** @@ -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..f77a81922 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|null */ 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; } @@ -66,10 +69,10 @@ public function addRoute( #[Language('TEXT')] string $mask, array|string|\Closure $metadata = [], - int|bool $oneWay = 0, + bool $oneWay = false, ): static { - $this->add(new Route($mask, $metadata), (int) $oneWay); + $this->add(new Route($mask, $metadata), $oneWay); return $this; } @@ -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 = get_class($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..7ac5bd8b2 100644 --- a/src/Application/UI/AccessPolicy.php +++ b/src/Application/UI/AccessPolicy.php @@ -62,12 +62,15 @@ private function getAttributes(): array } + /** + * @param Attributes\Requires[] $attrs + * @return Attributes\Requires[] + */ private function applyInternalRules(array $attrs, Component $component): array { if ( $this->element instanceof \ReflectionMethod && str_starts_with($this->element->getName(), $component::formatSignalMethod('')) - && !ComponentReflection::parseAnnotation($this->element, 'crossOrigin') && !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->sameOrigin === false) ) { $attrs[] = new Attributes\Requires(sameOrigin: true); diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index e4feedfe2..1b4f26d5f 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, count, func_get_arg, 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); } @@ -72,18 +69,28 @@ public function getUniqueId(): string } + public function addComponent( + Nette\ComponentModel\IComponent $component, + ?string $name, + ?string $insertBefore = null, + ): static + { + if (!$component instanceof SignalReceiver && !$component instanceof StatePersistent) { + throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' is not intended to be used in the Presenter.'); + } + + return parent::addComponent($component, $name, $insertBefore = null); + } + + protected function createComponent(string $name): ?Nette\ComponentModel\IComponent { if (method_exists($this, $method = 'createComponent' . $name)) { - (new AccessPolicy(new \ReflectionMethod($this, $method)))->checkAccess($this); - } - $res = parent::createComponent($name); - if ($res && !$res instanceof SignalReceiver && !$res instanceof StatePersistent) { - $type = $res::class; - trigger_error("It seems that component '$name' of type $type is not intended to be used in the Presenter."); + $rm = new \ReflectionMethod($this, $method); + (new AccessPolicy($rm))->checkAccess($this); + $this->checkRequirements($rm); } - - return $res; + return parent::createComponent($name); } @@ -99,10 +106,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 +154,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'])) { @@ -162,9 +171,9 @@ public function loadState(array $params): void )); } - $this->$name = $params[$name]; + $this->$name = &$params[$name]; } else { - $params[$name] = $this->$name ?? null; + $params[$name] = &$this->$name; } } @@ -174,6 +183,7 @@ public function loadState(array $params): void /** * Saves state information for next request. + * @param array $params */ public function saveState(array &$params): void { @@ -183,6 +193,7 @@ public function saveState(array &$params): void /** * @internal used by presenter + * @param array $params */ public function saveStatePartial(array &$params, ComponentReflection $reflection): void { @@ -226,6 +237,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,10 +246,11 @@ final public function getParameter(string $name): mixed /** * Returns component parameters. + * @return array */ final public function getParameters(): array { - return $this->params; + return array_map(fn($item) => $item, $this->params); } @@ -260,7 +273,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."); } @@ -282,19 +295,19 @@ public static function formatSignalMethod(string $signal): string /** * Generates URL to presenter, action or signal. * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" - * @param array|mixed $args + * @param mixed ...$args * @throws InvalidLinkException */ - public function link(string $destination, $args = []): string + public function link(string $destination, ...$args): string { try { - $args = func_num_args() < 3 && is_array($args) - ? $args - : array_slice(func_get_args(), 1); + $args = count($args) === 1 && is_array($args[0] ?? null) + ? $args[0] + : $args; return $this->getPresenter()->getLinkGenerator()->link($destination, $args, $this, 'link'); } catch (InvalidLinkException $e) { - return $this->getPresenter()->handleInvalidLink($e); + return $this->getPresenter()->processInvalidLink($e); } } @@ -302,13 +315,13 @@ public function link(string $destination, $args = []): string /** * Returns destination as Link object. * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" - * @param array|mixed $args + * @param mixed ...$args */ - public function lazyLink(string $destination, $args = []): Link + public function lazyLink(string $destination, ...$args): Link { - $args = func_num_args() < 3 && is_array($args) - ? $args - : array_slice(func_get_args(), 1); + $args = count($args) === 1 && is_array($args[0] ?? null) + ? $args[0] + : $args; return new Link($this, $destination, $args); } @@ -316,15 +329,15 @@ public function lazyLink(string $destination, $args = []): Link /** * Determines whether it links to the current page. * @param ?string $destination in format "[[[module:]presenter:]action | signal! | this]" - * @param array|mixed $args + * @param mixed ...$args * @throws InvalidLinkException */ - public function isLinkCurrent(?string $destination = null, $args = []): bool + public function isLinkCurrent(?string $destination = null, ...$args): bool { if ($destination !== null) { - $args = func_num_args() < 3 && is_array($args) - ? $args - : array_slice(func_get_args(), 1); + $args = count($args) === 1 && is_array($args[0] ?? null) + ? $args[0] + : $args; $this->getPresenter()->getLinkGenerator()->createRequest($this, $destination, $args, 'test'); } @@ -335,15 +348,14 @@ public function isLinkCurrent(?string $destination = null, $args = []): bool /** * Redirect to another presenter, action or signal. * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" - * @param array|mixed $args - * @return never + * @param mixed ...$args * @throws Nette\Application\AbortException */ - public function redirect(string $destination, $args = []): void + public function redirect(string $destination, ...$args): never { - $args = func_num_args() < 3 && is_array($args) - ? $args - : array_slice(func_get_args(), 1); + $args = count($args) === 1 && is_array($args[0] ?? null) + ? $args[0] + : $args; $presenter = $this->getPresenter(); $presenter->saveGlobalState(); $presenter->redirectUrl($presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect')); @@ -353,15 +365,14 @@ public function redirect(string $destination, $args = []): void /** * Permanently redirects to presenter, action or signal. * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" - * @param array|mixed $args - * @return never + * @param mixed ...$args * @throws Nette\Application\AbortException */ - public function redirectPermanent(string $destination, $args = []): void + public function redirectPermanent(string $destination, ...$args): never { - $args = func_num_args() < 3 && is_array($args) - ? $args - : array_slice(func_get_args(), 1); + $args = count($args) === 1 && is_array($args[0] ?? null) + ? $args[0] + : $args; $presenter = $this->getPresenter(); $presenter->redirectUrl( $presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect'), @@ -374,11 +385,8 @@ public function redirectPermanent(string $destination, $args = []): void * Throws HTTP error. * @throws Nette\Application\BadRequestException */ - public function error(string $message = '', int $httpCode = Nette\Http\IResponse::S404_NotFound): void + public function error(string $message = '', int $httpCode = Nette\Http\IResponse::S404_NotFound): never { 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..4296731cf 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,7 +29,7 @@ final class ComponentReflection extends \ReflectionClass /** - * Returns array of class properties that are public and have attribute #[Persistent] or #[Parameter] or annotation @persistent. + * Returns array of class properties that are public and have attribute #[Persistent] or #[Parameter]. * @return array */ public function getParameters(): array @@ -44,10 +44,7 @@ public function getParameters(): array foreach ($this->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { if ($prop->isStatic()) { continue; - } elseif ( - self::parseAnnotation($prop, 'persistent') - || $prop->getAttributes(Attributes\Persistent::class) - ) { + } elseif ($prop->getAttributes(Attributes\Persistent::class)) { $params[$prop->getName()] = [ 'def' => $prop->hasDefaultValue() ? $prop->getDefaultValue() : null, 'type' => ParameterConverter::getType($prop), @@ -77,7 +74,7 @@ public function getParameters(): array /** - * Returns array of persistent properties. They are public and have attribute #[Persistent] or annotation @persistent. + * Returns array of persistent properties. They are public and have attribute #[Persistent]. * @return array */ public function getPersistentParams(): array @@ -88,7 +85,7 @@ public function getPersistentParams(): array /** * Returns array of persistent components. They are tagged with class-level attribute - * #[Persistent] or annotation @persistent or returned by Presenter::getPersistentComponents(). + * #[Persistent] or returned by Presenter::getPersistentComponents(). * @return array */ public function getPersistentComponents(): array @@ -100,9 +97,7 @@ public function getPersistentComponents(): array } $attrs = $this->getAttributes(Attributes\Persistent::class); - $names = $attrs - ? $attrs[0]->getArguments() - : (array) self::parseAnnotation($this, 'persistent'); + $names = $attrs ? $attrs[0]->getArguments() : []; $names = array_merge($names, $class::getPersistentComponents()); $components = array_fill_keys($names, ['since' => $class]); @@ -115,6 +110,9 @@ public function getPersistentComponents(): array } + /** + * @return string[] names of public properties with #[TemplateVariable] attribute + */ public function getTemplateVariables(Control $control): array { $res = []; @@ -167,73 +165,9 @@ public function getSignalMethod(string $signal): ?\ReflectionMethod } - /** - * Returns an annotation value. - */ - public static function parseAnnotation(\Reflector $ref, string $name): ?array - { - if (!preg_match_all('#[\s*]@' . preg_quote($name, '#') . '(?:\(\s*([^)]*)\s*\)|\s|$)#', (string) $ref->getDocComment(), $m)) { - return null; - } - - $tokens = ['true' => true, 'false' => false, 'null' => null]; - $res = []; - foreach ($m[1] as $s) { - foreach (preg_split('#\s*,\s*#', $s, -1, PREG_SPLIT_NO_EMPTY) ?: ['true'] as $item) { - $res[] = array_key_exists($tmp = strtolower($item), $tokens) - ? $tokens[$tmp] - : $item; - } - } - - return $res; - } - - - /** - * Has class specified annotation? - */ - public function hasAnnotation(string $name): bool - { - return (bool) self::parseAnnotation($this, $name); - } - - - /** - * Returns an annotation value. - */ - public function getAnnotation(string $name): mixed - { - $res = self::parseAnnotation($this, $name); - return $res ? end($res) : null; - } - - - public function getMethod($name): MethodReflection - { - return new MethodReflection($this->getName(), $name); - } - - - /** - * @return MethodReflection[] - */ - public function getMethods($filter = -1): array - { - foreach ($res = parent::getMethods($filter) as $key => $val) { - $res[$key] = new MethodReflection($this->getName(), $val->getName()); - } - - return $res; - } - - - /** @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..7e06433d7 100644 --- a/src/Application/UI/Control.php +++ b/src/Application/UI/Control.php @@ -79,10 +79,7 @@ protected function checkTemplateClass(string $class): ?string } - /** - * Descendant can override this method to customize template compile-time filters. - * @deprecated - */ + #[\Deprecated] public function templatePrepareFilters(Template $template): void { } diff --git a/src/Application/UI/Form.php b/src/Application/UI/Form.php index a5be276de..f9d85049f 100644 --- a/src/Application/UI/Form.php +++ b/src/Application/UI/Form.php @@ -62,14 +62,9 @@ protected function validateParent(Nette\ComponentModel\IContainer $parent): void /** * Returns the presenter where this component belongs to. */ - final public function getPresenter(): ?Presenter + final 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); } @@ -98,7 +93,7 @@ public function isAnchored(): bool } - /** @deprecated use allowCrossOrigin() */ + #[\Deprecated('use allowCrossOrigin()')] public function disableSameSiteProtection(): void { $this->allowCrossOrigin(); diff --git a/src/Application/UI/Link.php b/src/Application/UI/Link.php index 68eea2d1c..e974b9745 100644 --- a/src/Application/UI/Link.php +++ b/src/Application/UI/Link.php @@ -19,6 +19,7 @@ final class Link public function __construct( private readonly Component $component, private readonly string $destination, + /** @var array */ private array $params = [], ) { } @@ -63,6 +64,7 @@ public function getParameter(string $key): mixed /** * Returns link parameters. + * @return array */ public function getParameters(): array { diff --git a/src/Application/UI/MethodReflection.php b/src/Application/UI/MethodReflection.php deleted file mode 100644 index f576deeaf..000000000 --- a/src/Application/UI/MethodReflection.php +++ /dev/null @@ -1,35 +0,0 @@ - $args + * @return mixed[] + */ public static function toArguments(\ReflectionFunctionAbstract $method, array $args): array { $res = []; @@ -60,6 +64,7 @@ public static function toArguments(\ReflectionFunctionAbstract $method, array $a /** * Converts list of arguments to named parameters & check types. + * @param array $supplemental * @param \ReflectionParameter[] $missing arguments * @throws InvalidLinkException * @internal diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 7c4af760a..32752f7f2 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -23,12 +23,12 @@ /** * Presenter component represents a webpage instance. It converts Request to Response. * - * @property-read Nette\Application\Request $request - * @property-read string $action - * @property string $view - * @property string|bool $layout + * @property-deprecated Nette\Application\Request $request + * @property-deprecated string $action + * @property-deprecated string $view + * @property-deprecated string|bool $layout * @property-read \stdClass $payload - * @property-read Nette\Http\Session $session + * @property-deprecated Nette\Http\Session $session * @property-read Nette\Security\User $user */ abstract class Presenter extends Control implements Application\IPresenter @@ -96,7 +96,11 @@ abstract class Presenter extends Control implements Application\IPresenter public array $allowedMethods = ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH']; private ?Nette\Application\Request $request = null; private ?Nette\Application\Response $response = null; + + /** @var array> */ private array $globalParams = []; + + /** @var array */ private array $globalState; private ?array $globalStateSinces; private string $action = ''; @@ -116,11 +120,6 @@ abstract class Presenter extends Control implements Application\IPresenter private readonly LinkGenerator $linkGenerator; - public function __construct() - { - } - - final public function getRequest(): ?Application\Request { return $this->request; @@ -472,18 +471,26 @@ public function setLayout(string|bool $layout): static /** * @throws Nette\Application\AbortException - * @return never */ - public function sendTemplate(?Template $template = null): void + public function sendTemplate(?Template $template = null): never { $template ??= $this->getTemplate(); + $this->completeTemplate($template); + $this->sendResponse(new Responses\TextResponse($template)); + } + + + /** + * Completes template parameters and file before rendering. + */ + protected function completeTemplate(Template $template): void + { foreach ($this->getReflection()->getTemplateVariables($this) as $name) { $template->$name ??= $this->$name; } if ($template->getFile() === null) { $template->setFile($this->findTemplateFile()); } - $this->sendResponse(new Responses\TextResponse($template)); } @@ -649,9 +656,8 @@ public function isAjax(): bool /** * Sends AJAX payload to the output. * @throws Nette\Application\AbortException - * @return never */ - public function sendPayload(): void + public function sendPayload(): never { $this->sendResponse(new Responses\JsonResponse($this->getPayload())); } @@ -660,9 +666,8 @@ public function sendPayload(): void /** * Sends JSON data to the output. * @throws Nette\Application\AbortException - * @return never */ - public function sendJson(mixed $data): void + public function sendJson(mixed $data): never { $this->sendResponse(new Responses\JsonResponse($data)); } @@ -674,9 +679,8 @@ public function sendJson(mixed $data): void /** * Sends response and terminates presenter. * @throws Nette\Application\AbortException - * @return never */ - public function sendResponse(Application\Response $response): void + public function sendResponse(Application\Response $response): never { $this->response = $response; $this->terminate(); @@ -686,9 +690,8 @@ public function sendResponse(Application\Response $response): void /** * Correctly terminates presenter. * @throws Nette\Application\AbortException - * @return never */ - public function terminate(): void + public function terminate(): never { throw new Application\AbortException; } @@ -698,9 +701,8 @@ public function terminate(): void * Forward to another presenter or action. * @param array|mixed $args * @throws Nette\Application\AbortException - * @return never */ - public function forward(string|Nette\Application\Request $destination, $args = []): void + public function forward(string|Nette\Application\Request $destination, $args = []): never { if ($destination instanceof Application\Request) { $this->sendResponse(new Responses\ForwardResponse($destination)); @@ -717,9 +719,8 @@ public function forward(string|Nette\Application\Request $destination, $args = [ /** * Redirect to another URL and ends presenter execution. * @throws Nette\Application\AbortException - * @return never */ - public function redirectUrl(string $url, ?int $httpCode = null): void + public function redirectUrl(string $url, ?int $httpCode = null): never { if ($this->isAjax()) { $this->getPayload()->redirect = $url; @@ -814,23 +815,26 @@ public function lastModified( } - /** @deprecated @internal */ + #[\Deprecated] protected function createRequest(Component $component, string $destination, array $args, string $mode): ?string { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); return $this->linkGenerator->link($destination, $args, $component, $mode); } - /** @deprecated @internal */ + #[\Deprecated] public static function parseDestination(string $destination): array { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); return LinkGenerator::parseDestination($destination); } - /** @deprecated @internal */ + #[\Deprecated] protected function requestToUrl(Application\Request $request, ?bool $relative = null): string { + trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); return $this->linkGenerator->requestToUrl($request, $relative ?? !$this->absoluteUrls); } @@ -839,7 +843,7 @@ protected function requestToUrl(Application\Request $request, ?bool $relative = * Invalid link handler. Descendant can override this method to change default behaviour. * @throws InvalidLinkException */ - protected function handleInvalidLink(InvalidLinkException $e): string + protected function processInvalidLink(InvalidLinkException $e): string { if ($this->invalidLinkMode & self::InvalidLinkException) { throw $e; @@ -912,6 +916,7 @@ public static function getPersistentComponents(): array /** * Saves state information for all subcomponents to $this->globalState. + * @return array */ public function getGlobalState(?string $forClass = null): array { @@ -1061,6 +1066,7 @@ private function initGlobalParameters(): void /** * Pops parameters for specified component. + * @return array * @internal */ final public function popGlobalParameters(string $id): array diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php index d05e39b19..8dca9c25e 100644 --- a/src/Bridges/ApplicationDI/ApplicationExtension.php +++ b/src/Bridges/ApplicationDI/ApplicationExtension.php @@ -25,8 +25,11 @@ */ final class ApplicationExtension extends Nette\DI\CompilerExtension { + /** @var string[] */ private readonly array $scanDirs; private int $invalidLinkMode; + + /** @var array */ private array $checked = []; @@ -97,7 +100,7 @@ public function loadConfiguration(): void ->setType(Nette\Application\IPresenterFactory::class) ->setFactory(Nette\Application\PresenterFactory::class, [new Definitions\Statement( Nette\Bridges\ApplicationDI\PresenterFactoryCallback::class, - [1 => $this->invalidLinkMode, $touch ?? null], + [1 => $touch ?? null], )]); if ($config->mapping) { @@ -276,7 +279,7 @@ private function checkPresenter(string $class): void $re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.'; foreach ($rc->getMethods() as $rm) { - if (preg_match("#^(?!handleInvalidLink)($re)#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) { + if (preg_match("#^$re#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) { throw new Nette\InvalidStateException(sprintf('Method %s: this method must be public non-static.', Reflection::toString($rm))); } elseif (preg_match('#^createComponent.#', $rm->getName()) && ($rm->isPrivate() || $rm->isStatic())) { throw new Nette\InvalidStateException(sprintf('Method %s: this method must be non-private non-static.', Reflection::toString($rm))); diff --git a/src/Bridges/ApplicationDI/LatteExtension.php b/src/Bridges/ApplicationDI/LatteExtension.php index efc3d22ca..b7939d848 100644 --- a/src/Bridges/ApplicationDI/LatteExtension.php +++ b/src/Bridges/ApplicationDI/LatteExtension.php @@ -15,7 +15,7 @@ use Nette\DI\Definitions\Statement; use Nette\Schema\Expect; use Tracy; -use function class_exists, explode, is_string, str_contains, version_compare; +use function class_exists, is_string; /** @@ -34,7 +34,6 @@ public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ 'debugger' => Expect::anyOf(true, false, 'all'), - 'macros' => Expect::arrayOf('string'), 'extensions' => Expect::arrayOf('string|Nette\DI\Definitions\Statement'), 'templateClass' => Expect::string(), 'strictTypes' => Expect::bool(false), @@ -54,40 +53,30 @@ public function loadConfiguration(): void $config = $this->config; $builder = $this->getContainerBuilder(); - $latteFactory = $builder->addFactoryDefinition($this->prefix('latteFactory')) + $builder->addFactoryDefinition($this->prefix('latteFactory')) ->setImplement(ApplicationLatte\LatteFactory::class) ->getResultDefinition() ->setFactory(Latte\Engine::class) ->addSetup('setTempDirectory', [$this->tempDir]) ->addSetup('setAutoRefresh', [$this->debugMode]) - ->addSetup('setStrictTypes', [$config->strictTypes]); - - if (version_compare(Latte\Engine::VERSION, '3', '<')) { - foreach ($config->macros as $macro) { - $this->addMacro($macro); - } - } else { - $latteFactory->addSetup('setStrictParsing', [$config->strictParsing]) + ->addSetup('setStrictTypes', [$config->strictTypes]) + ->addSetup('setStrictParsing', [$config->strictParsing]) ->addSetup('enablePhpLinter', [$config->phpLinter]) - ->addSetup('setLocale', [$config->locale]); - - $builder->getDefinition($this->prefix('latteFactory')) - ->getResultDefinition() - ->addSetup('?', [$builder::literal('func_num_args() && $service->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension(func_get_arg(0)))')]); + ->addSetup('setLocale', [$config->locale]) + ->addSetup('addExtension', [new Statement(ApplicationLatte\UIExtension::class, [$builder::literal('$control')])]); - if ($builder->getByType(Nette\Caching\Storage::class)) { - $this->addExtension(new Statement(Nette\Bridges\CacheLatte\CacheExtension::class)); - } - if (class_exists(Nette\Bridges\FormsLatte\FormsExtension::class)) { - $this->addExtension(new Statement(Nette\Bridges\FormsLatte\FormsExtension::class)); - } + if ($builder->getByType(Nette\Caching\Storage::class)) { + $this->addExtension(new Statement(Nette\Bridges\CacheLatte\CacheExtension::class)); + } + if (class_exists(Nette\Bridges\FormsLatte\FormsExtension::class)) { + $this->addExtension(new Statement(Nette\Bridges\FormsLatte\FormsExtension::class)); + } - foreach ($config->extensions as $extension) { - if ($extension === Latte\Essential\TranslatorExtension::class) { - $extension = new Statement($extension, [new Nette\DI\Definitions\Reference(Nette\Localization\Translator::class)]); - } - $this->addExtension($extension); + foreach ($config->extensions as $extension) { + if ($extension === Latte\Essential\TranslatorExtension::class) { + $extension = new Statement($extension, [new Nette\DI\Definitions\Reference(Nette\Localization\Translator::class)]); } + $this->addExtension($extension); } $builder->addDefinition($this->prefix('templateFactory')) @@ -130,40 +119,12 @@ public static function initLattePanel( $control = $template->getLatte()->getProviders()['uiControl'] ?? null; if ($all || $control instanceof Nette\Application\UI\Presenter) { $name = $all && $control ? (new \ReflectionObject($control))->getShortName() : ''; - if (version_compare(Latte\Engine::VERSION, '3', '<')) { - $bar->addPanel(new Latte\Bridges\Tracy\LattePanel($template->getLatte(), $name)); - } else { - $template->getLatte()->addExtension(new Latte\Bridges\Tracy\TracyExtension($name)); - } + $template->getLatte()->addExtension(new Latte\Bridges\Tracy\TracyExtension($name)); } }; } - public function addMacro(string $macro): void - { - $builder = $this->getContainerBuilder(); - $definition = $builder->getDefinition($this->prefix('latteFactory'))->getResultDefinition(); - - if (($macro[0] ?? null) === '@') { - if (str_contains($macro, '::')) { - [$macro, $method] = explode('::', $macro); - } else { - $method = 'install'; - } - - $definition->addSetup('?->onCompile[] = function ($engine) { ?->' . $method . '($engine->getCompiler()); }', ['@self', $macro]); - - } else { - if (!str_contains($macro, '::') && class_exists($macro)) { - $macro .= '::install'; - } - - $definition->addSetup('?->onCompile[] = function ($engine) { ' . $macro . '($engine->getCompiler()); }', ['@self']); - } - } - - public function addExtension(Statement|string $extension): void { $extension = is_string($extension) diff --git a/src/Bridges/ApplicationDI/PresenterFactoryCallback.php b/src/Bridges/ApplicationDI/PresenterFactoryCallback.php index 5142af3b1..9d1f1d2dd 100644 --- a/src/Bridges/ApplicationDI/PresenterFactoryCallback.php +++ b/src/Bridges/ApplicationDI/PresenterFactoryCallback.php @@ -21,7 +21,6 @@ final class PresenterFactoryCallback { public function __construct( private readonly Nette\DI\Container $container, - private readonly int $invalidLinkMode, private readonly ?string $touchToRefresh, ) { } @@ -41,25 +40,13 @@ public function __invoke(string $class): Nette\Application\IPresenter return $this->container->createService($services[0]); } - if ($this->touchToRefresh) { + if ($this->touchToRefresh && class_exists($class)) { touch($this->touchToRefresh); + echo 'Class ' . htmlspecialchars($class) . ' was not found in DI container.

    If you just created this presenter, it should be enough to refresh the page. It will happen automatically in 5 seconds.

    Otherwise, please check the configuration of your DI container.'; + header('Refresh: 5'); + exit; } - try { - $presenter = $this->container->createInstance($class); - $this->container->callInjects($presenter); - } catch (Nette\DI\MissingServiceException | Nette\DI\ServiceCreationException $e) { - if ($this->touchToRefresh && class_exists($class)) { - throw new \Exception("Refresh your browser. New presenter $class was found.", 0, $e); - } - - throw $e; - } - - if ($presenter instanceof Nette\Application\UI\Presenter && !$presenter->invalidLinkMode) { - $presenter->invalidLinkMode = $this->invalidLinkMode; - } - - return $presenter; + throw new Nette\Application\InvalidPresenterException("No services of type $class found."); } } diff --git a/src/Bridges/ApplicationLatte/LatteFactory.php b/src/Bridges/ApplicationLatte/LatteFactory.php index e6d2c8842..71e1ced0c 100644 --- a/src/Bridges/ApplicationLatte/LatteFactory.php +++ b/src/Bridges/ApplicationLatte/LatteFactory.php @@ -10,11 +10,12 @@ namespace Nette\Bridges\ApplicationLatte; use Latte; +use Nette\Application\UI\Control; interface LatteFactory { - function create(/*?Control $control = null*/): Latte\Engine; + function create(?Control $control = null): Latte\Engine; } diff --git a/src/Bridges/ApplicationLatte/Nodes/ControlNode.php b/src/Bridges/ApplicationLatte/Nodes/ControlNode.php index 2a28c4640..bd05f3979 100644 --- a/src/Bridges/ApplicationLatte/Nodes/ControlNode.php +++ b/src/Bridges/ApplicationLatte/Nodes/ControlNode.php @@ -64,11 +64,9 @@ public static function create(Tag $tag): static } $modifier = $tag->parser->parseModifier(); - foreach ($modifier->filters as $filter) { - match ($filter->name->name) { - 'noescape' => $node->escape = false, - default => throw new Latte\CompileException('Only modifier |noescape is allowed here.', $tag->position), - }; + $node->escape = $modifier->removeFilter('noescape') ? false : null; + if ($modifier->filters) { + throw new Latte\CompileException('Only modifier |noescape is allowed here.', reset($modifier->filters)->position); } return $node; diff --git a/src/Bridges/ApplicationLatte/Nodes/IfCurrentNode.php b/src/Bridges/ApplicationLatte/Nodes/IfCurrentNode.php deleted file mode 100644 index dd1c6813f..000000000 --- a/src/Bridges/ApplicationLatte/Nodes/IfCurrentNode.php +++ /dev/null @@ -1,70 +0,0 @@ -position->line})", E_USER_DEPRECATED); - $node = $tag->node = new static; - if (!$tag->parser->isEnd()) { - $node->destination = $tag->parser->parseUnquotedStringOrExpression(); - $tag->parser->stream->tryConsume(','); - $node->args = $tag->parser->parseArguments(); - } - - [$node->content] = yield; - return $node; - } - - - public function print(PrintContext $context): string - { - return $this->destination - ? $context->format( - 'if ($this->global->uiPresenter->isLinkCurrent(%node, %args?)) { %node } ', - $this->destination, - $this->args, - $this->content, - ) - : $context->format( - 'if ($this->global->uiPresenter->getLastCreatedRequestFlag("current")) { %node } ', - $this->content, - ); - } - - - public function &getIterator(): \Generator - { - if ($this->destination) { - yield $this->destination; - yield $this->args; - } - yield $this->content; - } -} diff --git a/src/Bridges/ApplicationLatte/Nodes/LinkNode.php b/src/Bridges/ApplicationLatte/Nodes/LinkNode.php index e4320167b..aba74a25d 100644 --- a/src/Bridges/ApplicationLatte/Nodes/LinkNode.php +++ b/src/Bridges/ApplicationLatte/Nodes/LinkNode.php @@ -40,7 +40,6 @@ public static function create(Tag $tag): ?static $node->args = $tag->parser->parseArguments(); $node->modifier = $tag->parser->parseModifier(); $node->modifier->escape = true; - $node->modifier->check = false; $node->mode = $tag->name; if ($tag->isNAttribute()) { diff --git a/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php b/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php deleted file mode 100644 index 151913a07..000000000 --- a/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php +++ /dev/null @@ -1,61 +0,0 @@ -getParameters(), ' . PhpHelpers::dump($this->template ?? Template::class) . '); exit;'; - } - - - public static function printClass(array $params, string $parentClass): void - { - $bp = new Latte\Essential\Blueprint; - if (!method_exists($bp, 'generateTemplateClass')) { - throw new \LogicException("Please update 'latte/latte' to version 3.0.15 or newer."); - } - - $control = $params['control'] ?? $params['presenter'] ?? null; - $name = 'Template'; - if ($control instanceof UI\Control) { - $name = preg_replace('#(Control|Presenter)$#', '', $control::class) . 'Template'; - unset($params[$control instanceof UI\Presenter ? 'control' : 'presenter']); - } - $class = $bp->generateTemplateClass($params, $name, $parentClass); - $code = (string) $class->getNamespace(); - - $bp->printBegin(); - $bp->printCode($code); - - if ($control instanceof UI\Control) { - $file = dirname((new \ReflectionClass($control))->getFileName()) . '/' . $class->getName() . '.php'; - if (file_exists($file)) { - echo "unsaved, file {$bp->clickableFile($file)} already exists"; - } else { - echo "saved to file {$bp->clickableFile($file)}"; - file_put_contents($file, "printEnd(); - exit; - } -} diff --git a/src/Bridges/ApplicationLatte/SnippetBridge.php b/src/Bridges/ApplicationLatte/SnippetBridge.php deleted file mode 100644 index f5d62ef80..000000000 --- a/src/Bridges/ApplicationLatte/SnippetBridge.php +++ /dev/null @@ -1,95 +0,0 @@ -control = $control; - } - - - public function isSnippetMode(): bool - { - return (bool) $this->control->snippetMode; - } - - - public function setSnippetMode($snippetMode) - { - $this->control->snippetMode = $snippetMode; - } - - - public function needsRedraw($name): bool - { - return $this->control->isControlInvalid($name); - } - - - public function markRedrawn($name): void - { - if ($name !== '') { - $this->control->redrawControl($name, false); - } - } - - - public function getHtmlId($name): string - { - return $this->control->getSnippetId($name); - } - - - public function addSnippet($name, $content): void - { - if (!isset($this->payload)) { - $this->payload = $this->control->getPresenter()->getPayload(); - } - - $this->payload->snippets[$this->control->getSnippetId($name)] = $content; - } - - - public function renderChildren(): void - { - $queue = [$this->control]; - do { - foreach (array_shift($queue)->getComponents() as $child) { - if ($child instanceof Renderable) { - if ($child->isControlInvalid()) { - $child->snippetMode = true; - $child->render(); - $child->snippetMode = false; - } - } elseif ($child instanceof Nette\ComponentModel\IContainer) { - $queue[] = $child; - } - } - } while ($queue); - } -} diff --git a/src/Bridges/ApplicationLatte/SnippetRuntime.php b/src/Bridges/ApplicationLatte/SnippetRuntime.php index 71867a384..a3429f297 100644 --- a/src/Bridges/ApplicationLatte/SnippetRuntime.php +++ b/src/Bridges/ApplicationLatte/SnippetRuntime.php @@ -65,7 +65,7 @@ public function enter(string $name, string $type): void $this->stack[] = [$name, $obStarted]; if ($name !== '') { - $this->control->redrawControl($name, false); + $this->control->redrawControl($name, redraw: false); } } diff --git a/src/Bridges/ApplicationLatte/Template.php b/src/Bridges/ApplicationLatte/Template.php index cf7cef940..c5d429a64 100644 --- a/src/Bridges/ApplicationLatte/Template.php +++ b/src/Bridges/ApplicationLatte/Template.php @@ -11,7 +11,6 @@ use Latte; use Nette; -use function version_compare; /** @@ -36,6 +35,7 @@ final public function getLatte(): Latte\Engine /** * Renders template to output. + * @param array $params */ public function render(?string $file = null, array $params = []): void { @@ -46,6 +46,7 @@ public function render(?string $file = null, array $params = []): void /** * Renders template to output. + * @param array $params */ public function renderToString(?string $file = null, array $params = []): string { @@ -91,16 +92,7 @@ public function addFunction(string $name, callable $callback): static */ public function setTranslator(?Nette\Localization\Translator $translator, ?string $language = null): static { - if (version_compare(Latte\Engine::VERSION, '3', '<')) { - $this->latte->addFilter( - 'translate', - fn(Latte\Runtime\FilterInfo $fi, ...$args): string => $translator === null - ? $args[0] - : $translator->translate(...$args), - ); - } else { - $this->latte->addExtension(new Latte\Essential\TranslatorExtension($translator, $language)); - } + $this->latte->addExtension(new Latte\Essential\TranslatorExtension($translator, $language)); return $this; } @@ -126,6 +118,7 @@ final public function getFile(): ?string /** * Returns array of all parameters. + * @return array */ final public function getParameters(): array { diff --git a/src/Bridges/ApplicationLatte/TemplateFactory.php b/src/Bridges/ApplicationLatte/TemplateFactory.php index f60226a3c..70a828ea5 100644 --- a/src/Bridges/ApplicationLatte/TemplateFactory.php +++ b/src/Bridges/ApplicationLatte/TemplateFactory.php @@ -9,10 +9,9 @@ namespace Nette\Bridges\ApplicationLatte; -use Latte; use Nette; use Nette\Application\UI; -use function array_unshift, class_exists, is_a, iterator_to_array, preg_match, preg_replace, property_exists, rtrim, version_compare; +use function class_exists, is_a, preg_replace, property_exists, rtrim; /** @@ -29,10 +28,9 @@ public function __construct( private readonly LatteFactory $latteFactory, private readonly ?Nette\Http\IRequest $httpRequest = null, private readonly ?Nette\Security\User $user = null, - private readonly ?Nette\Caching\Storage $cacheStorage = null, $templateClass = null, ) { - if ($templateClass && (!class_exists($templateClass) || !is_a($templateClass, Template::class, true))) { + if ($templateClass && (!class_exists($templateClass) || !is_a($templateClass, Template::class, allow_string: true))) { throw new Nette\InvalidArgumentException("Class $templateClass does not implement " . Template::class . ' or it does not exist.'); } @@ -45,7 +43,7 @@ public function __construct( * @param class-string|null $class * @return T */ - public function createTemplate(?UI\Control $control = null, ?string $class = null): UI\Template + public function createTemplate(?UI\Control $control = null, ?string $class = null): Template { $class ??= $this->templateClass; if (!is_a($class, Template::class, allow_string: true)) { @@ -54,13 +52,6 @@ public function createTemplate(?UI\Control $control = null, ?string $class = nul $latte = $this->latteFactory->create($control); $template = new $class($latte); - - if (version_compare(Latte\Engine::VERSION, '3', '<')) { - $this->setupLatte2($latte, $control, $template); - } elseif (!Nette\Utils\Arrays::some($latte->getExtensions(), fn($e) => $e instanceof UIExtension)) { - $latte->addExtension(new UIExtension($control)); - } - $this->injectDefaultVariables($template, $control); Nette\Utils\Arrays::invoke($this->onCreate, $template); @@ -97,55 +88,4 @@ private function injectDefaultVariables(Template $template, ?UI\Control $control } } } - - - private function setupLatte2( - Latte\Engine $latte, - ?UI\Control $control, - Template $template, - ): void - { - if ($latte->onCompile instanceof \Traversable) { - $latte->onCompile = iterator_to_array($latte->onCompile); - } - - array_unshift($latte->onCompile, function (Latte\Engine $latte) use ($control, $template): void { - if ($this->cacheStorage) { - $latte->getCompiler()->addMacro('cache', new Nette\Bridges\CacheLatte\CacheMacro); - } - - UIMacros::install($latte->getCompiler()); - if (class_exists(Nette\Bridges\FormsLatte\FormMacros::class)) { - Nette\Bridges\FormsLatte\FormMacros::install($latte->getCompiler()); - } - - $control?->templatePrepareFilters($template); - }); - - $latte->addProvider('cacheStorage', $this->cacheStorage); - - $presenter = $control?->getPresenterIfExists(); - if ($control) { - $latte->addProvider('uiControl', $control); - $latte->addProvider('uiPresenter', $presenter); - $latte->addProvider('snippetBridge', new SnippetBridge($control)); - if ($presenter) { - $header = $presenter->getHttpResponse()->getHeader('Content-Security-Policy') - ?: $presenter->getHttpResponse()->getHeader('Content-Security-Policy-Report-Only'); - } - - $nonce = $presenter && preg_match('#\s\'nonce-([\w+/]+=*)\'#', (string) $header, $m) ? $m[1] : null; - $latte->addProvider('uiNonce', $nonce); - } - - if ($presenter) { - $latte->addFunction('isLinkCurrent', [$presenter, 'isLinkCurrent']); - $latte->addFunction('isModuleCurrent', [$presenter, 'isModuleCurrent']); - } - - $latte->addFilter('modifyDate', fn($time, $delta, $unit = null) => $time - ? Nette\Utils\DateTime::from($time)->modify($delta . $unit) - : null); - - } } diff --git a/src/Bridges/ApplicationLatte/UIExtension.php b/src/Bridges/ApplicationLatte/UIExtension.php index 965d83c03..ac07a7587 100644 --- a/src/Bridges/ApplicationLatte/UIExtension.php +++ b/src/Bridges/ApplicationLatte/UIExtension.php @@ -80,8 +80,6 @@ public function getTags(): array 'plink' => Nodes\LinkNode::create(...), 'link' => Nodes\LinkNode::create(...), 'linkBase' => Nodes\LinkBaseNode::create(...), - 'ifCurrent' => Nodes\IfCurrentNode::create(...), - 'templatePrint' => Nodes\TemplatePrintNode::create(...), 'snippet' => Nodes\SnippetNode::create(...), 'snippetArea' => Nodes\SnippetAreaNode::create(...), 'layout' => $this->createExtendsNode(...), @@ -94,7 +92,7 @@ public function getPasses(): array { return [ 'snippetRendering' => $this->snippetRenderingPass(...), - 'applyLinkBase' => [Nodes\LinkBaseNode::class, 'applyLinkBasePass'], + 'applyLinkBase' => Nodes\LinkBaseNode::applyLinkBasePass(...), ]; } diff --git a/src/Bridges/ApplicationLatte/UIMacros.php b/src/Bridges/ApplicationLatte/UIMacros.php deleted file mode 100644 index 131c30075..000000000 --- a/src/Bridges/ApplicationLatte/UIMacros.php +++ /dev/null @@ -1,187 +0,0 @@ -addMacro('control', [$me, 'macroControl']); - - $me->addMacro('href', null, null, fn(MacroNode $node, PhpWriter $writer): string => ' ?> href="macroLink($node, $writer) . ' ?>"addMacro('plink', [$me, 'macroLink']); - $me->addMacro('link', [$me, 'macroLink']); - $me->addMacro('ifCurrent', [$me, 'macroIfCurrent'], '}'); // deprecated; use n:class="$presenter->linkCurrent ? ..." - $me->addMacro('extends', [$me, 'macroExtends']); - $me->addMacro('layout', [$me, 'macroExtends']); - $me->addMacro('nonce', null, null, 'echo $this->global->uiNonce ? " nonce=\"{$this->global->uiNonce}\"" : "";'); - $me->addMacro('templatePrint', [$me, 'macroTemplatePrint'], null, null, self::ALLOWED_IN_HEAD); - } - - - /** - * Initializes before template parsing. - */ - public function initialize(): void - { - $this->extends = false; - } - - - /** - * Finishes template parsing. - * @return array(prolog, epilog) - */ - public function finalize() - { - if ($this->printTemplate) { - return ["Nette\\Bridges\\ApplicationLatte\\UIRuntime::printClass(\$this, $this->printTemplate); exit;"]; - } - - return [$this->extends . 'Nette\Bridges\ApplicationLatte\UIRuntime::initialize($this, $this->parentName, $this->blocks);']; - } - - - /********************* macros ****************d*g**/ - - - /** - * {control name[:method] [params]} - */ - public function macroControl(MacroNode $node, PhpWriter $writer) - { - if ($node->context !== [Latte\Compiler::CONTENT_HTML, Latte\Compiler::CONTEXT_HTML_TEXT]) { - $escapeMod = Latte\Helpers::removeFilter($node->modifiers, 'noescape') ? '' : '|escape'; - } - - if ($node->modifiers) { - trigger_error('Modifiers are deprecated in ' . $node->getNotation(), E_USER_DEPRECATED); - } - - $node->modifiers .= $escapeMod ?? ''; - - $words = $node->tokenizer->fetchWords(); - if (!$words) { - throw new CompileException('Missing control name in {control}'); - } - - $name = $writer->formatWord($words[0]); - $method = ucfirst($words[1] ?? ''); - $method = Strings::match($method, '#^\w*$#D') - ? "render$method" - : "{\"render$method\"}"; - - $tokens = $node->tokenizer; - $pos = $tokens->position; - $wrap = false; - while ($tokens->nextToken()) { - if ($tokens->isCurrent('=>', '(expand)') && !$tokens->depth) { - $wrap = true; - break; - } - } - - $tokens->position = $pos; - $param = $wrap ? $writer->formatArray() : $writer->formatArgs(); - - return "/* line $node->startLine */ " - . ($name[0] === '$' ? "if (is_object($name)) \$_tmp = $name; else " : '') - . '$_tmp = $this->global->uiControl->getComponent(' . $name . '); ' - . 'if ($_tmp instanceof Nette\Application\UI\Renderable) $_tmp->redrawControl(null, false); ' - . ($node->modifiers === '' - ? "\$_tmp->$method($param);" - : $writer->write( - "ob_start(fn() => null); \$_tmp->$method($param); \$ʟ_fi = new LR\\FilterInfo(%var); echo %modifyContent(ob_get_clean());", - Latte\Engine::CONTENT_HTML, - ) - ); - } - - - /** - * {link destination [,] [params]} - * {plink destination [,] [params]} - * n:href="destination [,] [params]" - */ - public function macroLink(MacroNode $node, PhpWriter $writer) - { - $node->modifiers = preg_replace('#\|safeurl\s*(?=\||$)#Di', '', $node->modifiers); - return $writer->using($node, $this->getCompiler()) - ->write( - 'echo %escape(%modify(' - . ($node->name === 'plink' ? '$this->global->uiPresenter' : '$this->global->uiControl') - . '->link(%node.word, %node.array?)))' - . ($node->startLine ? " /* line $node->startLine */;" : ';'), - ); - } - - - /** - * {ifCurrent destination [,] [params]} - */ - public function macroIfCurrent(MacroNode $node, PhpWriter $writer) - { - if ($node->modifiers) { - throw new CompileException('Modifiers are not allowed in ' . $node->getNotation()); - } - - return $writer->write( - $node->args - ? 'if ($this->global->uiPresenter->isLinkCurrent(%node.word, %node.array?)) {' - : 'if ($this->global->uiPresenter->getLastCreatedRequestFlag("current")) {', - ); - } - - - /** - * {extends auto} - */ - public function macroExtends(MacroNode $node, PhpWriter $writer) - { - if ($node->modifiers || $node->parentNode || $node->args !== 'auto') { - return $this->extends = false; - } - - $this->extends = $writer->write('$this->parentName = $this->global->uiPresenter->findLayoutTemplateFile();'); - } - - - /** - * {templatePrint [parentClass | default]} - */ - public function macroTemplatePrint(MacroNode $node): void - { - if ($node->modifiers) { - throw new CompileException('Modifiers are not allowed in ' . $node->getNotation()); - } - - $this->printTemplate = var_export($node->tokenizer->fetchWord() ?: null, true); - } -} diff --git a/src/Bridges/ApplicationLatte/UIRuntime.php b/src/Bridges/ApplicationLatte/UIRuntime.php deleted file mode 100644 index ace3af72d..000000000 --- a/src/Bridges/ApplicationLatte/UIRuntime.php +++ /dev/null @@ -1,84 +0,0 @@ -global; - $blocks = array_filter(array_keys($blocks), fn(string $s): bool => $s[0] !== '_'); - if ( - $parentName === null - && $blocks - && !$template->getReferringTemplate() - && ($providers->uiControl ?? null) instanceof Nette\Application\UI\Presenter - ) { - $parentName = $providers->uiControl->findLayoutTemplateFile(); - } - } - - - public static function printClass(Latte\Runtime\Template $template, ?string $parent = null): void - { - $blueprint = new Latte\Runtime\Blueprint; - $name = 'Template'; - $params = $template->getParameters(); - $control = $params['control'] ?? $params['presenter'] ?? null; - if ($control) { - $name = preg_replace('#(Control|Presenter)$#', '', $control::class) . 'Template'; - unset($params[$control instanceof Presenter ? 'control' : 'presenter']); - } - - if ($parent) { - if (!class_exists($parent)) { - $blueprint->printHeader("{templatePrint}: Class '$parent' doesn't exist."); - return; - } - - $params = array_diff_key($params, get_class_vars($parent)); - } - - $funcs = array_diff_key((array) $template->global->fn, (new Latte\Runtime\Defaults)->getFunctions()); - unset($funcs['isLinkCurrent'], $funcs['isModuleCurrent']); - - $namespace = new Php\PhpNamespace(Php\Helpers::extractNamespace($name)); - $class = $namespace->addClass(Php\Helpers::extractShortName($name)); - $class->setExtends($parent ?: Template::class); - if (!$parent) { - $class->addTrait(Nette\SmartObject::class); - } - - $blueprint->addProperties($class, $params, true); - $blueprint->addFunctions($class, $funcs); - - $end = $blueprint->printCanvas(); - $blueprint->printHeader('Native types'); - $blueprint->printCode((string) $namespace); - - $blueprint->addProperties($class, $params, false); - - $blueprint->printHeader('phpDoc types'); - $blueprint->printCode((string) $namespace); - echo $end; - } -} diff --git a/src/Bridges/ApplicationTracy/RoutingPanel.php b/src/Bridges/ApplicationTracy/RoutingPanel.php index bbfd16777..aeb38e72b 100644 --- a/src/Bridges/ApplicationTracy/RoutingPanel.php +++ b/src/Bridges/ApplicationTracy/RoutingPanel.php @@ -20,7 +20,10 @@ */ final class RoutingPanel implements Tracy\IBarPanel { + /** @var array{path: string, domain: ?string, module: string, routes: array} */ private array $routes; + + /** @var array|null */ private ?array $matched = null; @@ -66,6 +69,7 @@ public function getPanel(): string } + /** @return array{path: string, domain: ?string, module: string, routes: array} */ private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRequest): array { $res = [ @@ -85,7 +89,7 @@ private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRe continue; } - $matched = $flags[$i] & $router::ONE_WAY ? 'oneway' : 'no'; + $matched = empty($flags[$i]['oneWay']) ? 'no' : 'oneway'; $params = $e = null; try { if ( diff --git a/src/compatibility-intf.php b/src/compatibility-intf.php index 5bee25bb1..0d5b2f8ff 100644 --- a/src/compatibility-intf.php +++ b/src/compatibility-intf.php @@ -9,17 +9,6 @@ namespace Nette\Application; -use Nette; - -if (false) { - /** @deprecated use Nette\Routing\Router */ - interface IRouter extends Nette\Routing\Router - { - } -} elseif (!interface_exists(IRouter::class)) { - class_alias(Nette\Routing\Router::class, IRouter::class); -} - if (false) { /** @deprecated use Nette\Application\Response */ interface IResponse extends Response diff --git a/src/compatibility.php b/src/compatibility.php deleted file mode 100644 index cf55aa4bc..000000000 --- a/src/compatibility.php +++ /dev/null @@ -1,28 +0,0 @@ - 'My\App\*Mod\*Presenter', ]); - Assert::same('FooPresenter', $factory->formatPresenterClass('Foo')); - Assert::same('FooModule\BarPresenter', $factory->formatPresenterClass('Foo:Bar')); - Assert::same('FooModule\BarModule\BazPresenter', $factory->formatPresenterClass('Foo:Bar:Baz')); + Assert::same('App\Presentation\Foo\FooPresenter', $factory->formatPresenterClass('Foo')); + Assert::same('App\Presentation\Foo\Bar\BarPresenter', $factory->formatPresenterClass('Foo:Bar')); + Assert::same('App\Presentation\Foo\Bar\Baz\BazPresenter', $factory->formatPresenterClass('Foo:Bar:Baz')); - Assert::same('Foo2Presenter', $factory->formatPresenterClass('Foo2')); + Assert::same('App\Presentation\Foo2\Foo2Presenter', $factory->formatPresenterClass('Foo2')); Assert::same('App2\BarPresenter', $factory->formatPresenterClass('Foo2:Bar')); Assert::same('App2\Bar\BazPresenter', $factory->formatPresenterClass('Foo2:Bar:Baz')); @@ -44,7 +44,7 @@ test('simple wildcard mapping', function () { 'Foo3' => 'My\App\*Presenter', ]); - Assert::same('Foo2Presenter', $factory->formatPresenterClass('Foo2')); + Assert::same('App\Presentation\Foo2\Foo2Presenter', $factory->formatPresenterClass('Foo2')); Assert::same('App2\BarPresenter', $factory->formatPresenterClass('Foo2:Bar')); Assert::same('App2\BarModule\BazPresenter', $factory->formatPresenterClass('Foo2:Bar:Baz')); @@ -61,11 +61,11 @@ test('multi-segment wildcard expansion', function () { 'Foo3' => 'My\App\*Mod\**Presenter', ]); - Assert::same('FooPresenter', $factory->formatPresenterClass('Foo')); - Assert::same('FooModule\BarPresenter', $factory->formatPresenterClass('Foo:Bar')); - Assert::same('FooModule\BarModule\BazPresenter', $factory->formatPresenterClass('Foo:Bar:Baz')); + Assert::same('App\Presentation\Foo\FooPresenter', $factory->formatPresenterClass('Foo')); + Assert::same('App\Presentation\Foo\Bar\BarPresenter', $factory->formatPresenterClass('Foo:Bar')); + Assert::same('App\Presentation\Foo\Bar\Baz\BazPresenter', $factory->formatPresenterClass('Foo:Bar:Baz')); - Assert::same('Foo2Presenter', $factory->formatPresenterClass('Foo2')); + Assert::same('App\Presentation\Foo2\Foo2Presenter', $factory->formatPresenterClass('Foo2')); Assert::same('App2\Bar\BarPresenter', $factory->formatPresenterClass('Foo2:Bar')); Assert::same('App2\Bar\Baz\BazPresenter', $factory->formatPresenterClass('Foo2:Bar:Baz')); diff --git a/tests/Bridges.DI/LatteExtension.2.phpt b/tests/Bridges.DI/LatteExtension.2.phpt deleted file mode 100644 index 8144950b6..000000000 --- a/tests/Bridges.DI/LatteExtension.2.phpt +++ /dev/null @@ -1,122 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -class LoremIpsumMacros extends Latte\Macros\MacroSet -{ - public static function install(Latte\Compiler $compiler) - { - $me = new static($compiler); - $me->addMacro('lorem', 'lorem'); - Notes::add($me::class); - } -} - - -class IpsumLoremMacros extends Latte\Macros\MacroSet -{ - public static function install(Latte\Compiler $compiler) - { - $me = new static($compiler); - $me->addMacro('ipsum', 'ipsum'); - Notes::add($me::class); - } -} - - -class FooMacros extends Latte\Macros\MacroSet -{ - public static function install(Latte\Compiler $compiler) - { - $me = new static($compiler); - $me->addMacro('foo', 'foo'); - Notes::add($me::class); - } -} - - -class NonStaticMacrosFactory -{ - private string $parameter; - - - public function __construct($parameter) - { - $this->parameter = $parameter; - } - - - public function install(Latte\Compiler $compiler) - { - $macros = new Latte\Macros\MacroSet($compiler); - $macros->addMacro('foo', 'foo ' . $this->parameter); - Notes::add(static::class . '::install'); - } - - - public function create(Latte\Compiler $compiler) - { - $macros = new Latte\Macros\MacroSet($compiler); - $macros->addMacro('foo2', 'foo ' . $this->parameter); - Notes::add(static::class . '::create'); - } -} - - -class AnotherExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - foreach ($this->compiler->getExtensions(Nette\Bridges\ApplicationDI\LatteExtension::class) as $extension) { - $extension->addMacro('FooMacros::install'); - } - } -} - - -$loader = new DI\Config\Loader; -$config = $loader->load(Tester\FileMock::create(' -latte: - macros: - - LoremIpsumMacros - - IpsumLoremMacros::install - - @macroFactory - - @macroFactory::create - -services: - macroFactory: NonStaticMacrosFactory(foo) -', 'neon')); - -$compiler = new DI\Compiler; -$compiler->addExtension('latte', new Nette\Bridges\ApplicationDI\LatteExtension('', false)); -$compiler->addExtension('another', new AnotherExtension); -$code = $compiler->addConfig($config)->compile(); -eval($code); - -$container = new Container; - - -Assert::type(Nette\Bridges\ApplicationLatte\LatteFactory::class, $container->getService('nette.latteFactory')); -$container->getService('nette.latteFactory')->create()->setLoader(new Latte\Loaders\StringLoader)->compile(''); - -Assert::same([ - 'LoremIpsumMacros', - 'IpsumLoremMacros', - 'NonStaticMacrosFactory::install', - 'NonStaticMacrosFactory::create', - 'FooMacros', -], Notes::fetch()); diff --git a/tests/Bridges.DI/LatteExtension.basic.phpt b/tests/Bridges.DI/LatteExtension.basic.phpt index 09dc0d292..a3a13d48f 100644 --- a/tests/Bridges.DI/LatteExtension.basic.phpt +++ b/tests/Bridges.DI/LatteExtension.basic.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class MyExtension extends Latte\Extension { @@ -63,20 +59,6 @@ Assert::type(Nette\Bridges\ApplicationLatte\LatteFactory::class, $container->get $latte = $container->getService('nette.latteFactory')->create(); $extensions = Assert::with($latte, fn() => $this->extensions); -Assert::equal([ - new Latte\Essential\CoreExtension, - new Latte\Sandbox\SandboxExtension, - new Nette\Bridges\FormsLatte\FormsExtension, - new MyExtension, - new MyExtension(1), - new MyExtension(2), - new MyExtension, -], $extensions); - -// UIExtension is added -$latte = $container->getService('nette.latteFactory')->create(null); -$extensions = Assert::with($latte, fn() => $this->extensions); - Assert::equal([ new Latte\Essential\CoreExtension, new Latte\Sandbox\SandboxExtension, diff --git a/tests/Bridges.DI/LatteExtension.translator.phpt b/tests/Bridges.DI/LatteExtension.translator.phpt index f555b460e..2a9555ff3 100644 --- a/tests/Bridges.DI/LatteExtension.translator.phpt +++ b/tests/Bridges.DI/LatteExtension.translator.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class Translator implements Nette\Localization\Translator { @@ -43,4 +39,4 @@ $container = new Container; $latte = $container->getService('nette.latteFactory')->create(); $extensions = Assert::with($latte, fn() => $this->extensions); -Assert::equal(new Latte\Essential\TranslatorExtension(new Translator), $extensions[3]); +Assert::equal(new Latte\Essential\TranslatorExtension(new Translator), $extensions[4]); diff --git a/tests/Bridges.DI/RoutingExtension.basic.phpt b/tests/Bridges.DI/RoutingExtension.basic.phpt index a8e13d1a6..0b6f57a72 100644 --- a/tests/Bridges.DI/RoutingExtension.basic.phpt +++ b/tests/Bridges.DI/RoutingExtension.basic.phpt @@ -31,9 +31,10 @@ test('', function () { $container = new Container1; $router = $container->getService('router'); Assert::type(Nette\Application\Routers\RouteList::class, $router); - Assert::same('index.php', $router[0]->getMask()); - Assert::same('item/', $router[1]->getMask()); + $routes = $router->getRouters(); + Assert::same('index.php', $routes[0]->getMask()); + Assert::same('item/', $routes[1]->getMask()); Assert::type(Nette\Application\Routers\RouteList::class, $router); - Assert::type(Nette\Application\Routers\Route::class, $router[0]); + Assert::type(Nette\Application\Routers\Route::class, $routes[0]); }); diff --git a/tests/Bridges.Latte3/ControlMock.php b/tests/Bridges.Latte/ControlMock.php similarity index 100% rename from tests/Bridges.Latte3/ControlMock.php rename to tests/Bridges.Latte/ControlMock.php diff --git a/tests/Bridges.Latte3/Template.getParentName().phpt b/tests/Bridges.Latte/Template.getParentName().phpt similarity index 95% rename from tests/Bridges.Latte3/Template.getParentName().phpt rename to tests/Bridges.Latte/Template.getParentName().phpt index 598fbdb97..a75350763 100644 --- a/tests/Bridges.Latte3/Template.getParentName().phpt +++ b/tests/Bridges.Latte/Template.getParentName().phpt @@ -6,10 +6,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - Tester\Environment::bypassFinals(); $presenter = Mockery::mock(Nette\Application\UI\Presenter::class) diff --git a/tests/Bridges.Latte3/TemplateFactory.customTemplate.phpt b/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt similarity index 94% rename from tests/Bridges.Latte3/TemplateFactory.customTemplate.phpt rename to tests/Bridges.Latte/TemplateFactory.customTemplate.phpt index e231cdb34..4df04c5a7 100644 --- a/tests/Bridges.Latte3/TemplateFactory.customTemplate.phpt +++ b/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt @@ -14,10 +14,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - Tester\Environment::bypassFinals(); diff --git a/tests/Bridges.Latte2/TemplateFactory.onCreate.phpt b/tests/Bridges.Latte/TemplateFactory.onCreate.phpt similarity index 89% rename from tests/Bridges.Latte2/TemplateFactory.onCreate.phpt rename to tests/Bridges.Latte/TemplateFactory.onCreate.phpt index 8f86ef42d..987e86aea 100644 --- a/tests/Bridges.Latte2/TemplateFactory.onCreate.phpt +++ b/tests/Bridges.Latte/TemplateFactory.onCreate.phpt @@ -14,10 +14,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '>')) { - Tester\Environment::skip('Test for Latte 2'); -} - test('', function () { $engine = new Latte\Engine; diff --git a/tests/Bridges.Latte3/TemplateFactory.templateVariables.84.phpt b/tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt similarity index 100% rename from tests/Bridges.Latte3/TemplateFactory.templateVariables.84.phpt rename to tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt diff --git a/tests/Bridges.Latte3/TemplateFactory.templateVariables.phpt b/tests/Bridges.Latte/TemplateFactory.templateVariables.phpt similarity index 100% rename from tests/Bridges.Latte3/TemplateFactory.templateVariables.phpt rename to tests/Bridges.Latte/TemplateFactory.templateVariables.phpt diff --git a/tests/Bridges.Latte3/UIExtension.filters.absoluteUrl.phpt b/tests/Bridges.Latte/UIExtension.filters.absoluteUrl.phpt similarity index 94% rename from tests/Bridges.Latte3/UIExtension.filters.absoluteUrl.phpt rename to tests/Bridges.Latte/UIExtension.filters.absoluteUrl.phpt index 6b1b6b63f..8b728ec3f 100644 --- a/tests/Bridges.Latte3/UIExtension.filters.absoluteUrl.phpt +++ b/tests/Bridges.Latte/UIExtension.filters.absoluteUrl.phpt @@ -10,10 +10,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - Tester\Environment::bypassFinals(); diff --git a/tests/Bridges.Latte3/UIExtension.filters.phpt b/tests/Bridges.Latte/UIExtension.filters.phpt similarity index 88% rename from tests/Bridges.Latte3/UIExtension.filters.phpt rename to tests/Bridges.Latte/UIExtension.filters.phpt index 952f5a760..0dc89f8cd 100644 --- a/tests/Bridges.Latte3/UIExtension.filters.phpt +++ b/tests/Bridges.Latte/UIExtension.filters.phpt @@ -10,10 +10,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $latte = new Latte\Engine; $latte->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension(null)); diff --git a/tests/Bridges.Latte3/UIExtension.nonce.control.phpt b/tests/Bridges.Latte/UIExtension.nonce.control.phpt similarity index 87% rename from tests/Bridges.Latte3/UIExtension.nonce.control.phpt rename to tests/Bridges.Latte/UIExtension.nonce.control.phpt index 9248da8af..cf822c0ab 100644 --- a/tests/Bridges.Latte3/UIExtension.nonce.control.phpt +++ b/tests/Bridges.Latte/UIExtension.nonce.control.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $response = Mockery::mock(Nette\Http\IResponse::class); $response->shouldReceive('getHeader')->with('Content-Security-Policy')->andReturn("hello 'nonce-abcd123==' world"); diff --git a/tests/Bridges.Latte3/UIExtension.nonce.presenter.phpt b/tests/Bridges.Latte/UIExtension.nonce.presenter.phpt similarity index 89% rename from tests/Bridges.Latte3/UIExtension.nonce.presenter.phpt rename to tests/Bridges.Latte/UIExtension.nonce.presenter.phpt index 2f2a8d254..15f0d212d 100644 --- a/tests/Bridges.Latte3/UIExtension.nonce.presenter.phpt +++ b/tests/Bridges.Latte/UIExtension.nonce.presenter.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - Tester\Environment::bypassFinals(); $response = Mockery::mock(Nette\Http\IResponse::class); diff --git a/tests/Bridges.Latte3/expected/ifCurrent.php b/tests/Bridges.Latte/expected/ifCurrent.php similarity index 100% rename from tests/Bridges.Latte3/expected/ifCurrent.php rename to tests/Bridges.Latte/expected/ifCurrent.php diff --git a/tests/Bridges.Latte/expected/isLinkCurrent.php b/tests/Bridges.Latte/expected/isLinkCurrent.php new file mode 100644 index 000000000..145117989 --- /dev/null +++ b/tests/Bridges.Latte/expected/isLinkCurrent.php @@ -0,0 +1,32 @@ +%A% + echo 'isLinkCurrent() ? 'current' : null])) ? ' class="' . LR\HtmlHelpers::escapeAttr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* pos 1:21 */; + echo '>n:href before n:class + +isLinkCurrent() ? 'current' : null])) ? ' class="' . LR\HtmlHelpers::escapeAttr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* pos 3:4 */; + echo '>n:href after n:class + +isLinkCurrent() ? 'current' : null])) ? ' class="' . LR\HtmlHelpers::escapeAttr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* pos 5:26 */; + echo '>href before n:class + +isLinkCurrent() ? 'current' : null])) ? ' class="' . LR\HtmlHelpers::escapeAttr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* pos 7:4 */; + echo ' href="'; + echo LR\HtmlHelpers::escapeAttr($this->global->uiControl->link('default')) /* pos 7:58 */; + echo '">href after n:class + +global->fn->isLinkCurrent)($this, 'default') ? 'current' : null])) ? ' class="' . LR\HtmlHelpers::escapeAttr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* pos 9:4 */; + echo '>custom function +'; +%A% diff --git a/tests/Bridges.Latte3/expected/n-snippet.block.php b/tests/Bridges.Latte/expected/n-snippet.block.php similarity index 76% rename from tests/Bridges.Latte3/expected/n-snippet.block.php rename to tests/Bridges.Latte/expected/n-snippet.block.php index c2a978759..156b09d4d 100644 --- a/tests/Bridges.Latte3/expected/n-snippet.block.php +++ b/tests/Bridges.Latte/expected/n-snippet.block.php @@ -11,12 +11,12 @@ final class Template%a% extends Latte\Runtime\Template public function main(array $ʟ_args): void { %A% - $this->renderBlock('block1', get_defined_vars()) /* %a% */; + $this->renderBlock('block1', get_defined_vars()) /* pos %d%:26 */; echo ' '; echo '
    '; - $this->renderBlock('outer', [], null, 'snippet') /* %a% */; + $this->renderBlock('outer', [], null, 'snippet') /* pos %d%:1 */; echo '
    '; } @@ -31,7 +31,7 @@ public function blockBlock1(array $ʟ_args): void echo ''; - $this->renderBlock('snippet', [], null, 'snippet') /* %a% */; + $this->renderBlock('snippet', [], null, 'snippet') /* pos %d%:6 */; echo ' '; } @@ -44,7 +44,7 @@ public function blockSnippet(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('snippet', 'static') /* %a% */; + $this->global->snippetDriver->enter('snippet', 'static') /* pos %d%:6 */; try { echo ' static @@ -63,12 +63,12 @@ public function blockOuter(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('outer', 'static') /* %a% */; + $this->global->snippetDriver->enter('outer', 'static') /* pos %d%:1 */; try { echo ' begin '; - $this->renderBlock('block2', get_defined_vars()) /* %a% */; + $this->renderBlock('block2', get_defined_vars()) /* pos %d%:30 */; echo 'end '; @@ -88,7 +88,7 @@ public function blockBlock2(array $ʟ_args): void echo ''; - $this->global->snippetDriver->enter($ʟ_nm, 'dynamic') /* %a% */; + $this->global->snippetDriver->enter($ʟ_nm, 'dynamic') /* pos %d%:6 */; try { echo ' dynamic diff --git a/tests/Bridges.Latte3/expected/n-snippet.dynamic.php b/tests/Bridges.Latte/expected/n-snippet.dynamic.php similarity index 73% rename from tests/Bridges.Latte3/expected/n-snippet.dynamic.php rename to tests/Bridges.Latte/expected/n-snippet.dynamic.php index 2383a6003..815837a6e 100644 --- a/tests/Bridges.Latte3/expected/n-snippet.dynamic.php +++ b/tests/Bridges.Latte/expected/n-snippet.dynamic.php @@ -12,7 +12,7 @@ public function main(array $ʟ_args): void %A% echo ' '; echo '
    '; - $this->renderBlock('outer1', [], null, 'snippet') /* %a% */; + $this->renderBlock('outer1', [], null, 'snippet') /* pos 1:2 */; echo '
    '; } @@ -30,18 +30,18 @@ public function blockOuter1(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('outer1', 'static') /* %a% */; + $this->global->snippetDriver->enter('outer1', 'static') /* pos 1:2 */; try { echo "\n"; - foreach ([1, 2, 3] as $id) /* %a% */ { + foreach ([1, 2, 3] as $id) /* pos 2:2 */ { echo ' '; - $this->global->snippetDriver->enter($ʟ_nm, 'dynamic') /* %a% */; + $this->global->snippetDriver->enter($ʟ_nm, 'dynamic') /* pos 3:8 */; try { echo ' #'; - echo LR\%a%Text($id) /* %a% */; + echo LR\HtmlHelpers::escapeText($id) /* pos 4:6 */; echo ' '; diff --git a/tests/Bridges.Latte3/expected/n-snippet.php b/tests/Bridges.Latte/expected/n-snippet.php similarity index 69% rename from tests/Bridges.Latte3/expected/n-snippet.php rename to tests/Bridges.Latte/expected/n-snippet.php index fcd2af5e2..399056402 100644 --- a/tests/Bridges.Latte3/expected/n-snippet.php +++ b/tests/Bridges.Latte/expected/n-snippet.php @@ -13,25 +13,26 @@ public function main(array $ʟ_args): void echo '
    '; - $this->renderBlock('outer', [], null, 'snippet') /* %a% */; + $this->renderBlock('outer', [], null, 'snippet') /* pos %d%:20 */; echo '
    renderBlock('gallery', [], null, 'snippet') /* %a% */; + echo LR\HtmlHelpers::formatListAttribute(' class', 'class') /* pos %d%:36 */; + echo '>'; + $this->renderBlock('gallery', [], null, 'snippet') /* pos %d%:7 */; echo ' '; - $this->renderBlock('script', [], null, 'snippet') /* %a% */; + $this->renderBlock('script', [], null, 'snippet') /* pos %d%:10 */; echo ' '; - $this->renderBlock('hello', [], null, 'snippet') /* %a% */; + $this->renderBlock('hello', [], null, 'snippet') /* pos %d%:10 */; echo ' '; } @@ -44,7 +45,7 @@ public function blockOuter(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('outer', 'static') /* %a% */; + $this->global->snippetDriver->enter('outer', 'static') /* pos %d%:20 */; try { echo '

    Outer

    @@ -63,7 +64,7 @@ public function blockGallery(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('gallery', 'static') /* %a% */; + $this->global->snippetDriver->enter('gallery', 'static') /* pos %d%:7 */; try { } finally { $this->global->snippetDriver->leave(); @@ -78,9 +79,9 @@ public function blockScript(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('script', 'static') /* %a% */; + $this->global->snippetDriver->enter('script', 'static') /* pos %d%:10 */; try { - echo LR\%a%::escapeJs('x') /* %a% */; + echo LR\Helpers::escapeJs('x') /* pos %d%:29 */; } finally { $this->global->snippetDriver->leave(); @@ -95,9 +96,9 @@ public function blockHello(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('hello', 'static') /* %a% */; + $this->global->snippetDriver->enter('hello', 'static') /* pos %d%:10 */; try { - echo LR\%a%::escapeJs('y') /* %a% */; + echo LR\Helpers::escapeJs('y') /* pos %d%:32 */; } finally { $this->global->snippetDriver->leave(); diff --git a/tests/Bridges.Latte3/expected/renderSnippets.html b/tests/Bridges.Latte/expected/renderSnippets.html similarity index 100% rename from tests/Bridges.Latte3/expected/renderSnippets.html rename to tests/Bridges.Latte/expected/renderSnippets.html diff --git a/tests/Bridges.Latte3/expected/snippet.dynamic.php b/tests/Bridges.Latte/expected/snippet.dynamic.php similarity index 73% rename from tests/Bridges.Latte3/expected/snippet.dynamic.php rename to tests/Bridges.Latte/expected/snippet.dynamic.php index c52e4b68a..27dbe6818 100644 --- a/tests/Bridges.Latte3/expected/snippet.dynamic.php +++ b/tests/Bridges.Latte/expected/snippet.dynamic.php @@ -12,7 +12,7 @@ public function main(array $ʟ_args): void %A% echo ' '; echo '
    '; - $this->renderBlock('outer', [], null, 'snippet') /* %a% */; + $this->renderBlock('outer', [], null, 'snippet') /* pos %d%:2 */; echo '
    '; } @@ -30,17 +30,17 @@ public function blockOuter(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('outer', 'static') /* %a% */; + $this->global->snippetDriver->enter('outer', 'static') /* pos %d%:2 */; try { echo "\n"; - foreach ([1, 2, 3] as $id) /* %a% */ { + foreach ([1, 2, 3] as $id) /* pos %d%:2 */ { echo ' '; echo '
    '; - $this->global->snippetDriver->enter($ʟ_nm, 'dynamic') /* %a% */; + $this->global->snippetDriver->enter($ʟ_nm, 'dynamic') /* pos %d%:3 */; try { echo ' #'; - echo LR\%a%Text($id) /* %a% */; + echo LR\HtmlHelpers::escapeText($id) /* pos %d%:6 */; echo ' '; diff --git a/tests/Bridges.Latte3/expected/snippet.dynamic2.php b/tests/Bridges.Latte/expected/snippet.dynamic2.php similarity index 73% rename from tests/Bridges.Latte3/expected/snippet.dynamic2.php rename to tests/Bridges.Latte/expected/snippet.dynamic2.php index b2d33178b..339c016a9 100644 --- a/tests/Bridges.Latte3/expected/snippet.dynamic2.php +++ b/tests/Bridges.Latte/expected/snippet.dynamic2.php @@ -12,7 +12,7 @@ public function main(array $ʟ_args): void %A% echo ' '; echo '
    '; - $this->renderBlock('outer', [], null, 'snippet') /* %a% */; + $this->renderBlock('outer', [], null, 'snippet') /* pos %d%:2 */; echo '
    '; } @@ -30,17 +30,17 @@ public function blockOuter(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('outer', 'static') /* %a% */; + $this->global->snippetDriver->enter('outer', 'static') /* pos %d%:2 */; try { echo "\n"; - foreach ([1, 2, 3] as $id) /* %a% */ { + foreach ([1, 2, 3] as $id) /* pos %d%:2 */ { echo ' '; echo '
    '; - $this->global->snippetDriver->enter($ʟ_nm, 'dynamic') /* %a% */; + $this->global->snippetDriver->enter($ʟ_nm, 'dynamic') /* pos %d%:3 */; try { echo ' #'; - echo LR\%a%Text($id) /* %a% */; + echo LR\HtmlHelpers::escapeText($id) /* pos %d%:6 */; echo ' '; diff --git a/tests/Bridges.Latte3/expected/snippet.php b/tests/Bridges.Latte/expected/snippet.php similarity index 74% rename from tests/Bridges.Latte3/expected/snippet.php rename to tests/Bridges.Latte/expected/snippet.php index 014f2d5ef..7baf9c070 100644 --- a/tests/Bridges.Latte3/expected/snippet.php +++ b/tests/Bridges.Latte/expected/snippet.php @@ -12,32 +12,32 @@ public function main(array $ʟ_args): void %A% echo ' '; echo '
    '; - $this->renderBlock('', [], null, 'snippet') /* %a% */; + $this->renderBlock('', [], null, 'snippet') /* pos %d%:2 */; echo '
    '; echo '
    '; - $this->renderBlock('outer', [], null, 'snippet') /* %a% */; + $this->renderBlock('outer', [], null, 'snippet') /* pos %d%:2 */; echo '
    @'; - if (true) /* %a% */ { + if (true) /* pos %d%:3 */ { echo ' Hello World @'; } echo ' '; echo '
    '; - $this->renderBlock('title', [], null, 'snippet') /* %a% */; + $this->renderBlock('title', [], null, 'snippet') /* pos %d%:2 */; echo '
    '; echo '
    '; - $this->renderBlock('title2', [], null, 'snippet') /* %a% */; + $this->renderBlock('title2', [], null, 'snippet') /* pos %d%:2 */; echo '
    '; } @@ -49,7 +49,7 @@ public function block1(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('', 'static') /* %a% */; + $this->global->snippetDriver->enter('', 'static') /* pos %d%:2 */; try { echo ' @@ -68,13 +68,13 @@ public function blockOuter(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('outer', 'static') /* %a% */; + $this->global->snippetDriver->enter('outer', 'static') /* pos %d%:2 */; try { echo ' Outer '; echo '
    '; - $this->renderBlock('inner', [], null, 'snippet') /* %a% */; + $this->renderBlock('inner', [], null, 'snippet') /* pos %d%:3 */; echo '
    /Outer '; @@ -92,7 +92,7 @@ public function blockInner(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('inner', 'static') /* %a% */; + $this->global->snippetDriver->enter('inner', 'static') /* pos %d%:3 */; try { echo 'Inner'; @@ -109,7 +109,7 @@ public function blockTitle(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('title', 'static') /* %a% */; + $this->global->snippetDriver->enter('title', 'static') /* pos %d%:2 */; try { echo 'Title 1'; @@ -126,7 +126,7 @@ public function blockTitle2(array $ʟ_args): void extract($ʟ_args); unset($ʟ_args); - $this->global->snippetDriver->enter('title2', 'static') /* %a% */; + $this->global->snippetDriver->enter('title2', 'static') /* pos %d%:2 */; try { echo 'Title 2'; diff --git a/tests/Bridges.Latte3/isLinkCurrent().phpt b/tests/Bridges.Latte/isLinkCurrent().phpt similarity index 91% rename from tests/Bridges.Latte3/isLinkCurrent().phpt rename to tests/Bridges.Latte/isLinkCurrent().phpt index 1ca37dcb3..277027dc9 100644 --- a/tests/Bridges.Latte3/isLinkCurrent().phpt +++ b/tests/Bridges.Latte/isLinkCurrent().phpt @@ -10,10 +10,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - Tester\Environment::bypassFinals(); $presenter = Mockery::mock(Nette\Application\UI\Presenter::class); diff --git a/tests/Bridges.Latte3/n-snippet.2.phpt b/tests/Bridges.Latte/n-snippet.2.phpt similarity index 93% rename from tests/Bridges.Latte3/n-snippet.2.phpt rename to tests/Bridges.Latte/n-snippet.2.phpt index 983c46ce5..a5cfc2a20 100644 --- a/tests/Bridges.Latte3/n-snippet.2.phpt +++ b/tests/Bridges.Latte/n-snippet.2.phpt @@ -6,10 +6,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $latte = new Latte\Engine; $latte->setLoader(new Latte\Loaders\StringLoader); $latte->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension(null)); diff --git a/tests/Bridges.Latte3/n-snippet.block.phpt b/tests/Bridges.Latte/n-snippet.block.phpt similarity index 83% rename from tests/Bridges.Latte3/n-snippet.block.phpt rename to tests/Bridges.Latte/n-snippet.block.phpt index 107dd6ebc..27a83c2f7 100644 --- a/tests/Bridges.Latte3/n-snippet.block.phpt +++ b/tests/Bridges.Latte/n-snippet.block.phpt @@ -6,10 +6,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $latte = new Latte\Engine; $latte->setLoader(new Latte\Loaders\StringLoader); diff --git a/tests/Bridges.Latte3/n-snippet.dynamic.phpt b/tests/Bridges.Latte/n-snippet.dynamic.phpt similarity index 82% rename from tests/Bridges.Latte3/n-snippet.dynamic.phpt rename to tests/Bridges.Latte/n-snippet.dynamic.phpt index ab2827bca..a794e2d0c 100644 --- a/tests/Bridges.Latte3/n-snippet.dynamic.phpt +++ b/tests/Bridges.Latte/n-snippet.dynamic.phpt @@ -6,10 +6,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $latte = new Latte\Engine; $latte->setLoader(new Latte\Loaders\StringLoader); diff --git a/tests/Bridges.Latte3/n-snippet.phpt b/tests/Bridges.Latte/n-snippet.phpt similarity index 85% rename from tests/Bridges.Latte3/n-snippet.phpt rename to tests/Bridges.Latte/n-snippet.phpt index c9ff1b893..ac2247eec 100644 --- a/tests/Bridges.Latte3/n-snippet.phpt +++ b/tests/Bridges.Latte/n-snippet.phpt @@ -6,9 +6,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} class Test { diff --git a/tests/Bridges.Latte3/renderSnippets.phpt b/tests/Bridges.Latte/renderSnippets.phpt similarity index 95% rename from tests/Bridges.Latte3/renderSnippets.phpt rename to tests/Bridges.Latte/renderSnippets.phpt index 675995784..4d113171f 100644 --- a/tests/Bridges.Latte3/renderSnippets.phpt +++ b/tests/Bridges.Latte/renderSnippets.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class InnerControl extends Nette\Application\UI\Control { diff --git a/tests/Bridges.Latte3/renderSnippets2.phpt b/tests/Bridges.Latte/renderSnippets2.phpt similarity index 93% rename from tests/Bridges.Latte3/renderSnippets2.phpt rename to tests/Bridges.Latte/renderSnippets2.phpt index 31522dbee..1a092d470 100644 --- a/tests/Bridges.Latte3/renderSnippets2.phpt +++ b/tests/Bridges.Latte/renderSnippets2.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class InnerControl extends Nette\Application\UI\Control { diff --git a/tests/Bridges.Latte3/renderSnippets3.phpt b/tests/Bridges.Latte/renderSnippets3.phpt similarity index 93% rename from tests/Bridges.Latte3/renderSnippets3.phpt rename to tests/Bridges.Latte/renderSnippets3.phpt index dd11322d4..65a621014 100644 --- a/tests/Bridges.Latte3/renderSnippets3.phpt +++ b/tests/Bridges.Latte/renderSnippets3.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class TestControl extends Nette\Application\UI\Control { diff --git a/tests/Bridges.Latte3/renderSnippets4.phpt b/tests/Bridges.Latte/renderSnippets4.phpt similarity index 91% rename from tests/Bridges.Latte3/renderSnippets4.phpt rename to tests/Bridges.Latte/renderSnippets4.phpt index f01809640..b300c03bc 100644 --- a/tests/Bridges.Latte3/renderSnippets4.phpt +++ b/tests/Bridges.Latte/renderSnippets4.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class TestPresenter extends Nette\Application\UI\Presenter { diff --git a/tests/Bridges.Latte3/renderSnippets5.phpt b/tests/Bridges.Latte/renderSnippets5.phpt similarity index 93% rename from tests/Bridges.Latte3/renderSnippets5.phpt rename to tests/Bridges.Latte/renderSnippets5.phpt index fdbd86159..340e36cf6 100644 --- a/tests/Bridges.Latte3/renderSnippets5.phpt +++ b/tests/Bridges.Latte/renderSnippets5.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class TestPresenter extends Nette\Application\UI\Presenter { diff --git a/tests/Bridges.Latte3/renderSnippets6.phpt b/tests/Bridges.Latte/renderSnippets6.phpt similarity index 91% rename from tests/Bridges.Latte3/renderSnippets6.phpt rename to tests/Bridges.Latte/renderSnippets6.phpt index f264135e7..a9cda7417 100644 --- a/tests/Bridges.Latte3/renderSnippets6.phpt +++ b/tests/Bridges.Latte/renderSnippets6.phpt @@ -11,10 +11,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class TestPresenter extends Nette\Application\UI\Presenter { diff --git a/tests/Bridges.Latte3/renderSnippets7.phpt b/tests/Bridges.Latte/renderSnippets7.phpt similarity index 98% rename from tests/Bridges.Latte3/renderSnippets7.phpt rename to tests/Bridges.Latte/renderSnippets7.phpt index fb7d7c175..e6792e74f 100644 --- a/tests/Bridges.Latte3/renderSnippets7.phpt +++ b/tests/Bridges.Latte/renderSnippets7.phpt @@ -9,10 +9,6 @@ Tester\Environment::bypassFinals(); require __DIR__ . '/ControlMock.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $dataSets = [ diff --git a/tests/Bridges.Latte2/templates/include3.latte b/tests/Bridges.Latte/templates/include3.latte similarity index 100% rename from tests/Bridges.Latte2/templates/include3.latte rename to tests/Bridges.Latte/templates/include3.latte diff --git a/tests/Bridges.Latte2/templates/snippet-include.latte b/tests/Bridges.Latte/templates/snippet-include.latte similarity index 100% rename from tests/Bridges.Latte2/templates/snippet-include.latte rename to tests/Bridges.Latte/templates/snippet-include.latte diff --git a/tests/Bridges.Latte2/templates/snippet-included.latte b/tests/Bridges.Latte/templates/snippet-included.latte similarity index 100% rename from tests/Bridges.Latte2/templates/snippet-included.latte rename to tests/Bridges.Latte/templates/snippet-included.latte diff --git a/tests/Bridges.Latte3/{control}.2.phpt b/tests/Bridges.Latte/{control}.2.phpt similarity index 94% rename from tests/Bridges.Latte3/{control}.2.phpt rename to tests/Bridges.Latte/{control}.2.phpt index bb2cb9171..7086c7fd1 100644 --- a/tests/Bridges.Latte3/{control}.2.phpt +++ b/tests/Bridges.Latte/{control}.2.phpt @@ -10,10 +10,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class MockComponent { diff --git a/tests/Bridges.Latte3/{control}.3.phpt b/tests/Bridges.Latte/{control}.3.phpt similarity index 84% rename from tests/Bridges.Latte3/{control}.3.phpt rename to tests/Bridges.Latte/{control}.3.phpt index fe9d4052f..b610ba991 100644 --- a/tests/Bridges.Latte3/{control}.3.phpt +++ b/tests/Bridges.Latte/{control}.3.phpt @@ -10,9 +10,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} $control = new class { public function render() @@ -52,5 +49,5 @@ Assert::same( Assert::exception( fn() => $latte->renderToString(''), Latte\RuntimeException::class, - 'Filters: unable to convert content type HTML to HTML/%a?%CSS', + 'Filters: unable to convert content type HTML to HTML/RAW/CSS', ); diff --git a/tests/Bridges.Latte3/{control}.phpt b/tests/Bridges.Latte/{control}.phpt similarity index 70% rename from tests/Bridges.Latte3/{control}.phpt rename to tests/Bridges.Latte/{control}.phpt index 9a6e1f4a6..678527d9b 100644 --- a/tests/Bridges.Latte3/{control}.phpt +++ b/tests/Bridges.Latte/{control}.phpt @@ -10,10 +10,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $latte = new Latte\Engine; $latte->setLoader(new Latte\Loaders\StringLoader); @@ -25,7 +21,7 @@ Assert::match( %A% $ʟ_tmp = $this->global->uiControl->getComponent('form'); if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); - $ʟ_tmp->render() /* %a% */; + $ʟ_tmp->render() /* pos 1:1 */; %A% XX, $latte->compile('{control form}'), @@ -36,7 +32,7 @@ Assert::match( %A% if (!is_object($ʟ_tmp = $form)) $ʟ_tmp = $this->global->uiControl->getComponent($ʟ_tmp); if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); - $ʟ_tmp->render() /* %a% */; + $ʟ_tmp->render() /* pos 1:1 */; %A% XX, $latte->compile('{control $form}'), @@ -45,9 +41,7 @@ Assert::match( Assert::match( <<<'XX' %A% - $ʟ_tmp = $this->global->uiControl->getComponent('form'); - %A% - $ʟ_tmp->renderType() /* %a% */; + $ʟ_tmp->renderType() /* pos 1:1 */; %A% XX, $latte->compile('{control form:type}'), @@ -56,9 +50,7 @@ Assert::match( Assert::match( <<<'XX' %A% - $ʟ_tmp = $this->global->uiControl->getComponent('form'); - %A% - $ʟ_tmp->{'render' . $type}() /* %a% */; + $ʟ_tmp->{'render' . $type}() /* pos 1:1 */; %A% XX, $latte->compile('{control form:$type}'), @@ -67,7 +59,7 @@ Assert::match( Assert::match( <<<'XX' %A% - $ʟ_tmp->renderType('param') /* %a% */; + $ʟ_tmp->renderType('param') /* pos 1:1 */; %A% XX, $latte->compile('{control form:type param}'), @@ -76,7 +68,7 @@ Assert::match( Assert::match( <<<'XX' %A% - $ʟ_tmp->render(...$params) /* %a% */; + $ʟ_tmp->render(...$params) /* pos 1:1 */; %A% XX, $latte->compile('{control form (expand) $params}'), @@ -85,7 +77,7 @@ Assert::match( Assert::match( <<<'XX' %A% - $ʟ_tmp->renderType(['param' => 123]) /* %a% */; + $ʟ_tmp->renderType(['param' => 123]) /* pos 1:1 */; %A% XX, $latte->compile('{control form:type param => 123}'), @@ -94,7 +86,7 @@ Assert::match( Assert::match( <<<'XX' %A% - $ʟ_tmp->renderType(param: 123) /* %a% */; + $ʟ_tmp->renderType(param: 123) /* pos 1:1 */; %A% XX, $latte->compile('{control form:type, param: 123}'), @@ -103,7 +95,7 @@ Assert::match( Assert::match( <<<'XX' %A% - $ʟ_tmp->renderType(param: 123) /* %a% */; + $ʟ_tmp->renderType(param: 123) /* pos 1:1 */; %A% XX, $latte->compile('{control form:type, param: 123}'), diff --git a/tests/Bridges.Latte3/{linkBase}.phpt b/tests/Bridges.Latte/{linkBase}.phpt similarity index 90% rename from tests/Bridges.Latte3/{linkBase}.phpt rename to tests/Bridges.Latte/{linkBase}.phpt index a1e9c0c34..78d6f66cf 100644 --- a/tests/Bridges.Latte3/{linkBase}.phpt +++ b/tests/Bridges.Latte/{linkBase}.phpt @@ -10,10 +10,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $latte = new Latte\Engine; $latte->setLoader(new Latte\Loaders\StringLoader); diff --git a/tests/Bridges.Latte3/{link}.2.phpt b/tests/Bridges.Latte/{link}.2.phpt similarity index 95% rename from tests/Bridges.Latte3/{link}.2.phpt rename to tests/Bridges.Latte/{link}.2.phpt index 816283b42..7fc270038 100644 --- a/tests/Bridges.Latte3/{link}.2.phpt +++ b/tests/Bridges.Latte/{link}.2.phpt @@ -10,10 +10,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - class MockControl { diff --git a/tests/Bridges.Latte3/{link}.phpt b/tests/Bridges.Latte/{link}.phpt similarity index 92% rename from tests/Bridges.Latte3/{link}.phpt rename to tests/Bridges.Latte/{link}.phpt index e2457c9a7..9d8e296e6 100644 --- a/tests/Bridges.Latte3/{link}.phpt +++ b/tests/Bridges.Latte/{link}.phpt @@ -10,10 +10,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $latte = new Latte\Engine; $latte->setLoader(new Latte\Loaders\StringLoader); diff --git a/tests/Bridges.Latte3/{snippet}.dynamic.phpt b/tests/Bridges.Latte/{snippet}.dynamic.phpt similarity index 87% rename from tests/Bridges.Latte3/{snippet}.dynamic.phpt rename to tests/Bridges.Latte/{snippet}.dynamic.phpt index 3b2068ed0..1eb5aacdd 100644 --- a/tests/Bridges.Latte3/{snippet}.dynamic.phpt +++ b/tests/Bridges.Latte/{snippet}.dynamic.phpt @@ -6,10 +6,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $latte = new Latte\Engine; $latte->setLoader(new Latte\Loaders\StringLoader); diff --git a/tests/Bridges.Latte3/{snippet}.phpt b/tests/Bridges.Latte/{snippet}.phpt similarity index 85% rename from tests/Bridges.Latte3/{snippet}.phpt rename to tests/Bridges.Latte/{snippet}.phpt index bf9022dce..5afa062ef 100644 --- a/tests/Bridges.Latte3/{snippet}.phpt +++ b/tests/Bridges.Latte/{snippet}.phpt @@ -6,10 +6,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -if (version_compare(Latte\Engine::VERSION, '3', '<')) { - Tester\Environment::skip('Test for Latte 3'); -} - $latte = new Latte\Engine; $latte->setLoader(new Latte\Loaders\StringLoader); diff --git a/tests/Bridges.Latte2/Template.getParentName().phpt b/tests/Bridges.Latte2/Template.getParentName().phpt deleted file mode 100644 index 1253c4a4e..000000000 --- a/tests/Bridges.Latte2/Template.getParentName().phpt +++ /dev/null @@ -1,58 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -$presenter = Mockery::mock(Nette\Application\UI\Presenter::class) - ->shouldReceive('findLayoutTemplateFile')->andReturn('layout.latte') - ->mock(); - -$latte = new Latte\Engine; -$latte->setLoader(new Latte\Loaders\StringLoader); -$latte->addProvider('uiControl', $presenter); -UIMacros::install($latte->getCompiler()); - -$template = $latte->createTemplate(''); -$template->prepare(); -Assert::null($template->getParentName()); - -$template = $latte->createTemplate('{block}...{/block}'); -$template->prepare(); -Assert::null($template->getParentName()); - -$template = $latte->createTemplate('{block name}...{/block}'); -$template->prepare(); -Assert::same('layout.latte', $template->getParentName()); - -$template = $latte->createTemplate('{extends "file.latte"} {block name}...{/block}'); -$template->prepare(); -Assert::same('file.latte', $template->getParentName()); - -$template = $latte->createTemplate('{extends "file.latte"}'); -$template->prepare(); -Assert::same('file.latte', $template->getParentName()); - -$template = $latte->createTemplate( - '{extends $file} {block name}...{/block}', - ['file' => 'file.latte'], -); -$template->prepare(); -Assert::same('file.latte', $template->getParentName()); - -$template = $latte->createTemplate('{extends none}'); -$template->prepare(); -Assert::null($template->getParentName()); - -$latte->addProvider('uiPresenter', $presenter); -$template = $latte->createTemplate('{extends auto}'); -$template->prepare(); -Assert::same('layout.latte', $template->getParentName()); diff --git a/tests/Bridges.Latte2/TemplateFactory.customTemplate.phpt b/tests/Bridges.Latte2/TemplateFactory.customTemplate.phpt deleted file mode 100644 index 011d5b52f..000000000 --- a/tests/Bridges.Latte2/TemplateFactory.customTemplate.phpt +++ /dev/null @@ -1,75 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - -Tester\Environment::bypassFinals(); - - -class TemplateMock extends Template -{ - private $file = 'ko'; - - - public function render(?string $file = null, array $params = []): void - { - echo strrev($this->file); - } - - - public function setFile(string $file): static - { - $this->file = $file; - return $this; - } - - - public function getFile(): string - { - return $this->file; - } -} - - -test('', function () { - $latteFactory = Mockery::mock(LatteFactory::class); - $latteFactory->shouldReceive('create')->andReturn(new Latte\Engine); - $factory = new TemplateFactory($latteFactory); - Assert::type(Template::class, $factory->createTemplate()); -}); - -Assert::exception(function () { - $factory = new TemplateFactory(Mockery::mock(LatteFactory::class), null, null, null, stdClass::class); -}, Nette\InvalidArgumentException::class, 'Class stdClass does not implement Nette\Bridges\ApplicationLatte\Template or it does not exist.'); - - -test('', function () { - $latteFactory = Mockery::mock(LatteFactory::class); - $latteFactory->shouldReceive('create')->andReturn(new Latte\Engine); - $factory = new TemplateFactory($latteFactory, null, null, null, TemplateMock::class); - $template = $factory->createTemplate(); - Assert::type(TemplateMock::class, $template); - Assert::type(UI\Template::class, $template); - ob_start(); - $template->render(); - Assert::same('ok', ob_get_clean()); - $template->setFile('bla'); - ob_start(); - $template->render(); - Assert::same('alb', ob_get_clean()); -}); diff --git a/tests/Bridges.Latte2/TemplateFactory.filters.phpt b/tests/Bridges.Latte2/TemplateFactory.filters.phpt deleted file mode 100644 index 57e879543..000000000 --- a/tests/Bridges.Latte2/TemplateFactory.filters.phpt +++ /dev/null @@ -1,47 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -class LatteFactory implements Nette\Bridges\ApplicationLatte\LatteFactory -{ - private $engine; - - - public function __construct(Latte\Engine $engine) - { - $this->engine = $engine; - } - - - public function create(): Latte\Engine - { - return $this->engine; - } -} - -$factory = new TemplateFactory(new LatteFactory(new Latte\Engine)); -$latte = $factory->createTemplate()->getLatte(); - - -setlocale(LC_TIME, 'C'); -date_default_timezone_set('Europe/Prague'); - -Assert::null($latte->invokeFilter('modifyDate', [null, null])); -Assert::same('1978-01-24 11:40:00', (string) $latte->invokeFilter('modifyDate', [254_400_000, '+1 day'])); -Assert::same('1978-05-06 00:00:00', (string) $latte->invokeFilter('modifyDate', ['1978-05-05', '+1 day'])); -Assert::same('1978-05-06 00:00:00', (string) $latte->invokeFilter('modifyDate', [new DateTime('1978-05-05'), '1day'])); -Assert::same('1978-01-22 11:40:00', (string) $latte->invokeFilter('modifyDate', [254_400_000, -1, 'day'])); diff --git a/tests/Bridges.Latte2/TemplateFactory.nonce.control.phpt b/tests/Bridges.Latte2/TemplateFactory.nonce.control.phpt deleted file mode 100644 index 850629b36..000000000 --- a/tests/Bridges.Latte2/TemplateFactory.nonce.control.phpt +++ /dev/null @@ -1,39 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - -$latte = new Latte\Engine; - -$latteFactory = Mockery::mock(ApplicationLatte\LatteFactory::class); -$latteFactory->shouldReceive('create')->andReturn($latte); - -$response = Mockery::mock(Nette\Http\IResponse::class); -$response->shouldReceive('getHeader')->with('Content-Security-Policy')->andReturn("hello 'nonce-abcd123==' world"); - -$control = Mockery::mock(UI\Control::class); -$control->shouldReceive('getPresenter')->andReturn(null); -$control->shouldIgnoreMissing(); - -$factory = new ApplicationLatte\TemplateFactory($latteFactory); -$factory->createTemplate($control); - -$latte->setLoader(new Latte\Loaders\StringLoader); - -Assert::match( - '', - $latte->renderToString(''), -); diff --git a/tests/Bridges.Latte2/TemplateFactory.nonce.presenter.phpt b/tests/Bridges.Latte2/TemplateFactory.nonce.presenter.phpt deleted file mode 100644 index 1ef2df0e8..000000000 --- a/tests/Bridges.Latte2/TemplateFactory.nonce.presenter.phpt +++ /dev/null @@ -1,42 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - -Tester\Environment::bypassFinals(); - -$latte = new Latte\Engine; - -$latteFactory = Mockery::mock(ApplicationLatte\LatteFactory::class); -$latteFactory->shouldReceive('create')->andReturn($latte); - -$response = Mockery::mock(Nette\Http\IResponse::class); -$response->shouldReceive('getHeader')->with('Content-Security-Policy')->andReturn("hello 'nonce-abcd123==' world"); - -$presenter = Mockery::mock(UI\Presenter::class); -$presenter->shouldReceive('getPresenterIfExists')->andReturn($presenter); -$presenter->shouldReceive('getHttpResponse')->andReturn($response); -$presenter->shouldIgnoreMissing(); - -$factory = new ApplicationLatte\TemplateFactory($latteFactory); -$factory->createTemplate($presenter); - -$latte->setLoader(new Latte\Loaders\StringLoader); - -Assert::match( - '', - $latte->renderToString(''), -); diff --git a/tests/Bridges.Latte2/TemplateFactory.onCompile.phpt b/tests/Bridges.Latte2/TemplateFactory.onCompile.phpt deleted file mode 100644 index b60ebd8e1..000000000 --- a/tests/Bridges.Latte2/TemplateFactory.onCompile.phpt +++ /dev/null @@ -1,80 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -test('', function () { - $engine = new Latte\Engine; - $latteFactory = Mockery::mock(LatteFactory::class); - $latteFactory->shouldReceive('create')->andReturn($engine); - $factory = new TemplateFactory($latteFactory, new Http\Request(new Http\UrlScript('http://nette.org'))); - $engine->onCompile[] = $callback = function () {}; - - $factory->createTemplate(); - - Assert::type('array', $engine->onCompile); - Assert::type(Closure::class, $engine->onCompile[0]); // prepended by TemplateFactory - Assert::same($callback, $engine->onCompile[1]); // our callback -}); - - -test('', function () { - $engine = new Latte\Engine; - $latteFactory = Mockery::mock(LatteFactory::class); - $latteFactory->shouldReceive('create')->andReturn($engine); - $factory = new TemplateFactory($latteFactory, new Http\Request(new Http\UrlScript('http://nette.org'))); - $engine->onCompile = new ArrayIterator([$callback = function () {}]); - - $factory->createTemplate(); - - Assert::type('array', $engine->onCompile); - Assert::type(Closure::class, $engine->onCompile[0]); // prepended by TemplateFactory - Assert::same($callback, $engine->onCompile[1]); // our callback -}); - - -test('', function () { - class Event implements IteratorAggregate - { - public $events; - - - public function __construct($events) - { - $this->events = $events; - } - - - public function getIterator(): ArrayIterator - { - return new ArrayIterator($this->events); - } - } - - $engine = new Latte\Engine; - $latteFactory = Mockery::mock(LatteFactory::class); - $latteFactory->shouldReceive('create')->andReturn($engine); - $factory = new TemplateFactory($latteFactory, new Http\Request(new Http\UrlScript('http://nette.org'))); - $engine->onCompile = new Event([$callback = function () {}]); - - $factory->createTemplate(); - - Assert::type('array', $engine->onCompile); - Assert::type(Closure::class, $engine->onCompile[0]); // prepended by TemplateFactory - Assert::same($callback, $engine->onCompile[1]); // our callback -}); diff --git a/tests/Bridges.Latte2/UIMacros.control.2.phpt b/tests/Bridges.Latte2/UIMacros.control.2.phpt deleted file mode 100644 index 9449e00e3..000000000 --- a/tests/Bridges.Latte2/UIMacros.control.2.phpt +++ /dev/null @@ -1,86 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -class MockComponent -{ - public function getComponent($name) - { - Notes::add(__METHOD__); - Notes::add(func_get_args()); - return new MockControl; - } -} - - -class MockControl -{ - public function __call($name, $args) - { - Notes::add(__METHOD__); - Notes::add(func_get_args()); - } -} - - -$latte = new Latte\Engine; -$latte->setLoader(new Latte\Loaders\StringLoader); -UIMacros::install($latte->getCompiler()); - -$latte->addProvider('uiControl', new MockComponent); -$params['form'] = new MockControl; -$params['name'] = 'form'; - -$latte->renderToString(' -{control \'name\'} - -{control form} - -{control form:test} - -{control $form:test} - -{control $name:test} - -{control $name:$name} - -{control form var1} - -{control form var1, 1, 2} - -{control form var1 => 5, 1, 2} -', $params); - -Assert::same([ - 'MockComponent::getComponent', ['name'], - 'MockControl::__call', ['render', []], - 'MockComponent::getComponent', ['form'], - 'MockControl::__call', ['render', []], - 'MockComponent::getComponent', ['form'], - 'MockControl::__call', ['renderTest', []], - 'MockControl::__call', ['renderTest', []], - 'MockComponent::getComponent', ['form'], - 'MockControl::__call', ['renderTest', []], - 'MockComponent::getComponent', ['form'], - 'MockControl::__call', ['renderform', []], - 'MockComponent::getComponent', ['form'], - 'MockControl::__call', ['render', ['var1']], - 'MockComponent::getComponent', ['form'], - 'MockControl::__call', ['render', ['var1', 1, 2]], - 'MockComponent::getComponent', ['form'], - 'MockControl::__call', ['render', [['var1' => 5, 0 => 1, 1 => 2]]], -], Notes::fetch()); diff --git a/tests/Bridges.Latte2/UIMacros.control.3.phpt b/tests/Bridges.Latte2/UIMacros.control.3.phpt deleted file mode 100644 index f5d6e23d2..000000000 --- a/tests/Bridges.Latte2/UIMacros.control.3.phpt +++ /dev/null @@ -1,51 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -$latte = new Latte\Engine; -$latte->setLoader(new Latte\Loaders\StringLoader); -UIMacros::install($latte->getCompiler()); -$latte->addProvider('uiControl', new class { - public function render() - { - echo '<>&'; - } - - - public function __call($name, $args) - { - return new self; - } -}); - -Assert::exception(function () use ($latte) { - $latte->renderToString('
    &', - $latte->renderToString('
    ', - $latte->renderToString('
    '), -); - -Assert::exception(function () use ($latte) { - $latte->renderToString(''); -}, Latte\RuntimeException::class, 'Filters: unable to convert content type HTML to HTMLCSS'); diff --git a/tests/Bridges.Latte2/UIMacros.control.phpt b/tests/Bridges.Latte2/UIMacros.control.phpt deleted file mode 100644 index 76f8e52f3..000000000 --- a/tests/Bridges.Latte2/UIMacros.control.phpt +++ /dev/null @@ -1,88 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -$latte = new Latte\Engine; -$latte->setLoader(new Latte\Loaders\StringLoader); -UIMacros::install($latte->getCompiler()); - -// {control ...} -Assert::match( - '%A% $this->global->uiControl->getComponent("form");%A%->render();%A%', - $latte->compile('{control form}'), -); - -@Assert::match( - <<<'XX' - %A% - /* %a% */ $_tmp = $this->global->uiControl->getComponent("form"); - if ($_tmp instanceof Nette\Application\UI\Renderable) $_tmp->redrawControl(null, false); - ob_start(fn() => null); - $_tmp->render(); - $ʟ_fi = new LR\FilterInfo('html'); - echo $this->filters->filterContent('filter', $ʟ_fi, ob_get_clean()); - %A% - XX, - $latte->compile('{control form|filter}'), -); // @deprecated - -Assert::match( - <<<'XX' - %A% - /* %a% */ if (is_object($form)) $_tmp = $form; - else $_tmp = $this->global->uiControl->getComponent($form); - if ($_tmp instanceof Nette\Application\UI\Renderable) $_tmp->redrawControl(null, false); - $_tmp->render(); - %A% - XX, - $latte->compile('{control $form}'), -); - -Assert::match( - '%A% $this->global->uiControl->getComponent("form");%A%->renderType();%A%', - $latte->compile('{control form:type}'), -); - -Assert::match( - '%A% $this->global->uiControl->getComponent("form");%A%->{"render$type"}();%A%', - $latte->compile('{control form:$type}'), -); - -Assert::match( - '%A% $this->global->uiControl->getComponent("form");%A%->renderType(\'param\');%A%', - $latte->compile('{control form:type param}'), -); - -Assert::match( - '%A% $this->global->uiControl->getComponent("form");%A%->render(array_merge([], $params, []));%A%', - $latte->compile('{control form (expand) $params}'), -); - -Assert::match( - '%A% $this->global->uiControl->getComponent("form");%A%->renderType([\'param\' => 123]);%A%', - $latte->compile('{control form:type param => 123}'), -); - -Assert::match( - '%A% $this->global->uiControl->getComponent("form");%A%->renderType([\'param\' => 123]);%A%', - $latte->compile('{control form:type, param => 123}'), -); - -Assert::match( - '%A% $this->global->uiControl->getComponent("form");%A%->renderType(param: 123);%A%', - $latte->compile('{control form:type, param: 123}'), -); diff --git a/tests/Bridges.Latte2/UIMacros.isLinkCurrent.phpt b/tests/Bridges.Latte2/UIMacros.isLinkCurrent.phpt deleted file mode 100644 index 579a8fd4b..000000000 --- a/tests/Bridges.Latte2/UIMacros.isLinkCurrent.phpt +++ /dev/null @@ -1,54 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - -Tester\Environment::bypassFinals(); - -$latte = new Latte\Engine; - -$latteFactory = Mockery::mock(Nette\Bridges\ApplicationLatte\LatteFactory::class); -$latteFactory->shouldReceive('create')->andReturn($latte); - -$presenter = Mockery::mock(Nette\Application\UI\Presenter::class); -$presenter->shouldReceive('getPresenterIfExists')->andReturn($presenter); -$presenter->shouldReceive('getHttpResponse')->andReturn((Mockery::mock(Nette\Http\IResponse::class))->shouldIgnoreMissing()); -$presenter->shouldIgnoreMissing(); - -$factory = new Nette\Bridges\ApplicationLatte\TemplateFactory($latteFactory); -$factory->createTemplate($presenter); - -$latte->setLoader(new Latte\Loaders\StringLoader); - -Assert::matchFile( - __DIR__ . '/expected/UIMacros.isLinkCurrent.php', - $latte->compile( - <<<'XX' - n:href before n:class - - n:href after n:class - - href before n:class - - href after n:class - - {ifCurrent}empty{/ifCurrent} - - {ifCurrent default}default{/ifCurrent} - - custom function - - XX, - ), -); diff --git a/tests/Bridges.Latte2/UIMacros.link.2.phpt b/tests/Bridges.Latte2/UIMacros.link.2.phpt deleted file mode 100644 index 83a441c31..000000000 --- a/tests/Bridges.Latte2/UIMacros.link.2.phpt +++ /dev/null @@ -1,122 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -class MockControl -{ - public function link($destination, $args = []) - { - if (!is_array($args)) { - $args = array_slice(func_get_args(), 1); - } - - array_unshift($args, $destination); - return 'link:' . strtr(json_encode($args), '"', "'"); - } -} - - -class MockPresenter extends MockControl -{ - public function link($destination, $args = []) - { - if (!is_array($args)) { - $args = array_slice(func_get_args(), 1); - } - - array_unshift($args, $destination); - return 'plink:' . strtr(json_encode($args), '"', "'"); - } - - - public function isAjax() - { - return false; - } -} - - -$latte = new Latte\Engine; -$latte->setLoader(new Latte\Loaders\StringLoader); -UIMacros::install($latte->getCompiler()); - -$latte->addProvider('uiControl', new MockControl); -$latte->addProvider('uiPresenter', new MockPresenter); -$params['action'] = 'login'; -$params['arr'] = ['link' => 'login', 'param' => 123]; - -Assert::match(<<<'EOD' - plink:['Homepage:'] - - plink:['Homepage:'] - - plink:['Homepage:action'] - - plink:['Homepage:action'] - - plink:['Homepage:action',10,20,'{one}&two'] - - plink:['Homepage:action#hash',10,20,'{one}&two'] - - plink:['#hash'] - - plink:[':',10] - - plink:{'0':'default','1':10,'a':20,'b':30} - - link:['login'] - - - - - - - - - - - EOD, strtr($latte->renderToString(<<<'EOD' - {plink Homepage:} - - {plink Homepage: } - - {plink Homepage:action } - - {plink 'Homepage:action' } - - {plink Homepage:action 10, 20, '{one}&two'} - - {plink Homepage:action#hash 10, 20, '{one}&two'} - - {plink #hash} - - {plink : 10 } - - {plink default 10, 'a' => 20, 'b' => 30} - - {link $action} - - - - - - - - - - - EOD, $params), [''' => "'", ''' => "'", '{' => '{'])); diff --git a/tests/Bridges.Latte2/UIMacros.link.phpt b/tests/Bridges.Latte2/UIMacros.link.phpt deleted file mode 100644 index 52fda3fb0..000000000 --- a/tests/Bridges.Latte2/UIMacros.link.phpt +++ /dev/null @@ -1,68 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -$latte = new Latte\Engine; -$latte->setLoader(new Latte\Loaders\StringLoader); -UIMacros::install($latte->getCompiler()); - -// {link ...} -Assert::contains( - '$this->global->uiControl->link("p")', - $latte->compile('{link p}'), -); -Assert::contains( - '($this->filters->filter)($this->global->uiControl->link("p"))', - $latte->compile('{link p|filter}'), -); -Assert::contains( - '$this->global->uiControl->link("p:a")', - $latte->compile('{link p:a}'), -); -Assert::contains( - '$this->global->uiControl->link($dest)', - $latte->compile('{link $dest}'), -); -Assert::contains( - '$this->global->uiControl->link($p:$a)', - $latte->compile('{link $p:$a}'), -); -Assert::contains( - '$this->global->uiControl->link("$p:$a")', - $latte->compile('{link "$p:$a"}'), -); -Assert::contains( - '$this->global->uiControl->link("p:a")', - $latte->compile('{link "p:a"}'), -); -Assert::contains( - '$this->global->uiControl->link(\'p:a\')', - $latte->compile('{link \'p:a\'}'), -); - -Assert::contains( - '$this->global->uiControl->link("p", [\'param\'])', - $latte->compile('{link p param}'), -); -Assert::contains( - '$this->global->uiControl->link("p", [\'param\' => 123])', - $latte->compile('{link p param => 123}'), -); -Assert::contains( - '$this->global->uiControl->link("p", [\'param\' => 123])', - $latte->compile('{link p, param => 123}'), -); diff --git a/tests/Bridges.Latte2/UIMacros.renderSnippets.phpt b/tests/Bridges.Latte2/UIMacros.renderSnippets.phpt deleted file mode 100644 index 71c354140..000000000 --- a/tests/Bridges.Latte2/UIMacros.renderSnippets.phpt +++ /dev/null @@ -1,97 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -class InnerControl extends Nette\Application\UI\Control -{ - public function render() - { - $latte = new Latte\Engine; - UIMacros::install($latte->getCompiler()); - $latte->addProvider('uiPresenter', $this->getPresenter()); - $latte->addProvider('uiControl', $this); - $latte->addProvider('snippetBridge', new Nette\Bridges\ApplicationLatte\SnippetBridge($this)); - $params['say'] = 'Hello'; - $latte->render(__DIR__ . '/templates/snippet-included.latte', $params); - } -} - -class TestPresenter extends Nette\Application\UI\Presenter -{ - public function createComponentMulti() - { - return new Nette\Application\UI\Multiplier(function () { - $control = new InnerControl; - $control->redrawControl(); - return $control; - }); - } - - - public function render() - { - $latte = new Latte\Engine; - UIMacros::install($latte->getCompiler()); - $latte->addProvider('snippetBridge', new Nette\Bridges\ApplicationLatte\SnippetBridge($this)); - $latte->render(__DIR__ . '/templates/snippet-include.latte'); - } -} - - -$presenter = new TestPresenter; -$presenter->snippetMode = true; -$presenter->redrawControl(); -$presenter['multi-1']->redrawControl(); -$presenter->render(); -Assert::same([ - 'snippets' => [ - 'snippet--hello' => 'Hello', - 'snippet--include' => "

    Included file #3 (A, B)

    \n", - 'snippet--array-1' => 'Value 1', - 'snippet--array-2' => 'Value 2', - 'snippet--array-3' => 'Value 3', - 'snippet--array2-1' => 'Value 1', - 'snippet--array2-2' => 'Value 2', - 'snippet--array2-3' => 'Value 3', - 'snippet--includeSay' => 'Hello include snippet', - 'snippet--nested1' => "\t
    Foo
    \n", - 'snippet-multi-1-includeSay' => 'Hello', - ], -], (array) $presenter->payload); - - - -$presenter = new TestPresenter; -$presenter->snippetMode = true; -$presenter->redrawControl('hello'); -$presenter->redrawControl('array'); -$presenter->render(); - -Assert::same([ - 'snippets' => [ - 'snippet--hello' => 'Hello', - 'snippet--array-1' => 'Value 1', - 'snippet--array-2' => 'Value 2', - 'snippet--array-3' => 'Value 3', - ], -], (array) $presenter->payload); - -$presenter = new TestPresenter; -ob_start(); -$presenter->render(); -$content = ob_get_clean(); -Assert::matchFile(__DIR__ . '/expected/UIMacros.renderSnippets.html', $content); diff --git a/tests/Bridges.Latte2/UIMacros.renderSnippets2.phpt b/tests/Bridges.Latte2/UIMacros.renderSnippets2.phpt deleted file mode 100644 index 58a72d170..000000000 --- a/tests/Bridges.Latte2/UIMacros.renderSnippets2.phpt +++ /dev/null @@ -1,84 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -class InnerControl extends Nette\Application\UI\Control -{ - public function render() - { - $this->renderA(); - $this->renderB(); - } - - - public function renderA() - { - $latte = new Latte\Engine; - $latte->setLoader(new Latte\Loaders\StringLoader); - UIMacros::install($latte->getCompiler()); - $latte->addProvider('uiPresenter', $this->getPresenter()); - $latte->addProvider('uiControl', $this); - $latte->addProvider('snippetBridge', new SnippetBridge($this)); - $params['say'] = 'Hello'; - $latte->render('{snippet testA}{$say}{/snippet}', $params); - } - - - public function renderB() - { - $latte = new Latte\Engine; - $latte->setLoader(new Latte\Loaders\StringLoader); - UIMacros::install($latte->getCompiler()); - $latte->addProvider('uiPresenter', $this->getPresenter()); - $latte->addProvider('uiControl', $this); - $latte->addProvider('snippetBridge', new SnippetBridge($this)); - $params['say'] = 'world'; - $latte->render('{snippet testB}{$say}{/snippet}', $params); - } -} - -class TestPresenter extends Nette\Application\UI\Presenter -{ - public function createComponentMulti() - { - return new Nette\Application\UI\Multiplier(fn() => new InnerControl); - } - - - public function render() - { - $latte = new Latte\Engine; - $latte->setLoader(new Latte\Loaders\StringLoader); - UIMacros::install($latte->getCompiler()); - $latte->addProvider('uiControl', $this); - $latte->addProvider('snippetBridge', new SnippetBridge($this)); - $latte->render(''); - } -} - - -$presenter = new TestPresenter; -$presenter->snippetMode = true; -$presenter['multi-1']->redrawControl(); -$presenter->render(); -Assert::same([ - 'snippets' => [ - 'snippet-multi-1-testA' => 'Hello', - 'snippet-multi-1-testB' => 'world', - ], -], (array) $presenter->payload); diff --git a/tests/Bridges.Latte2/UIMacros.renderSnippets3.phpt b/tests/Bridges.Latte2/UIMacros.renderSnippets3.phpt deleted file mode 100644 index 3dc72672b..000000000 --- a/tests/Bridges.Latte2/UIMacros.renderSnippets3.phpt +++ /dev/null @@ -1,73 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -class TestControl extends Nette\Application\UI\Control -{ - public function render() - { - $latte = new Latte\Engine; - $latte->setLoader(new Latte\Loaders\StringLoader); - UIMacros::install($latte->getCompiler()); - $latte->addProvider('uiPresenter', $this->getPresenter()); - $latte->addProvider('uiControl', $this); - $latte->addProvider('snippetBridge', new Nette\Bridges\ApplicationLatte\SnippetBridge($this)); - $latte->render('{snippet foo}hello{/snippet}'); - } -} - -class TestPresenter extends Nette\Application\UI\Presenter -{ - public function createComponentTest() - { - return new TestControl; - } - - - public function render() - { - $latte = new Latte\Engine; - $latte->setLoader(new Latte\Loaders\StringLoader); - UIMacros::install($latte->getCompiler()); - $latte->addProvider('uiControl', $this); - $latte->addProvider('snippetBridge', new Nette\Bridges\ApplicationLatte\SnippetBridge($this)); - $latte->render('{snippet foo}{control test}{/snippet}'); - } -} - - -$presenter = new TestPresenter; -$presenter->snippetMode = true; -$presenter->redrawControl('foo'); -$presenter['test']->redrawControl('foo'); -$presenter->render(); -Assert::same([ - 'snippets' => [ - 'snippet--foo' => '
    hello
    ', - ], -], (array) $presenter->payload); - - -$presenter = new TestPresenter; -$presenter->snippetMode = true; -$presenter['test']->redrawControl('foo'); -$presenter->render(); -Assert::same([ - 'snippets' => [ - 'snippet-test-foo' => 'hello', - ], -], (array) $presenter->payload); diff --git a/tests/Bridges.Latte2/UIMacros.renderSnippets4.phpt b/tests/Bridges.Latte2/UIMacros.renderSnippets4.phpt deleted file mode 100644 index 917404369..000000000 --- a/tests/Bridges.Latte2/UIMacros.renderSnippets4.phpt +++ /dev/null @@ -1,54 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - - -class TestPresenter extends Nette\Application\UI\Presenter -{ - public function render() - { - $latte = new Latte\Engine; - $latte->setLoader(new Latte\Loaders\StringLoader); - UIMacros::install($latte->getCompiler()); - $latte->addProvider('uiControl', $this); - $latte->addProvider('snippetBridge', new SnippetBridge($this)); - $latte->render('{snippet foo}{php $presenter->renderFoo()}{/snippet}', ['presenter' => $this]); - } - - - public function renderFoo() - { - $latte = new Latte\Engine; - $latte->setLoader(new Latte\Loaders\StringLoader); - UIMacros::install($latte->getCompiler()); - $latte->addProvider('uiControl', $this); - $latte->addProvider('snippetBridge', new SnippetBridge($this)); - $latte->render('Hello'); - } -} - - -$presenter = new TestPresenter; -$presenter->snippetMode = true; -$presenter->redrawControl('foo'); -$presenter->render(); -Assert::same([ - 'snippets' => [ - 'snippet--foo' => 'Hello', - ], -], (array) $presenter->payload); diff --git a/tests/Bridges.Latte2/UIMacros.renderSnippets5.phpt b/tests/Bridges.Latte2/UIMacros.renderSnippets5.phpt deleted file mode 100644 index a915657fe..000000000 --- a/tests/Bridges.Latte2/UIMacros.renderSnippets5.phpt +++ /dev/null @@ -1,59 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - - -class TestPresenter extends Nette\Application\UI\Presenter -{ - public function render(string $template) - { - $latte = new Latte\Engine; - $latte->setLoader(new Latte\Loaders\StringLoader); - UIMacros::install($latte->getCompiler()); - $latte->addProvider('uiControl', $this); - $latte->addProvider('snippetBridge', new SnippetBridge($this)); - $latte->render($template, ['presenter' => $this]); - } -} - - -$presenter = new TestPresenter; -$presenter->snippetMode = true; -$presenter->redrawControl('foo'); -$presenter->render('
    Hello
    '); -Assert::same([ - 'snippets' => [ - 'snippet--foo' => 'Hello', - ], -], (array) $presenter->payload); - - -$presenter = new TestPresenter; -$presenter->snippetMode = true; -$presenter->redrawControl('foo'); -Assert::exception(function () use ($presenter) { - $presenter->render('
    Hello
    '); -}, Latte\CompileException::class, 'Cannot combine HTML attribute id with n:snippet.'); - - -$presenter = new TestPresenter; -$presenter->snippetMode = true; -$presenter->redrawControl('foo'); -Assert::exception(function () use ($presenter) { - $presenter->render('
    Hello
    '); -}, Latte\CompileException::class, 'Cannot combine HTML attribute id with n:snippet.'); diff --git a/tests/Bridges.Latte2/UIMacros.renderSnippets6.phpt b/tests/Bridges.Latte2/UIMacros.renderSnippets6.phpt deleted file mode 100644 index dc4e32a25..000000000 --- a/tests/Bridges.Latte2/UIMacros.renderSnippets6.phpt +++ /dev/null @@ -1,45 +0,0 @@ -')) { - Tester\Environment::skip('Test for Latte 2'); -} - - -class TestPresenter extends Nette\Application\UI\Presenter -{ - public function render(string $template) - { - $latte = new Latte\Engine; - $latte->setLoader(new Latte\Loaders\StringLoader); - UIMacros::install($latte->getCompiler()); - $latte->addProvider('snippetBridge', new Nette\Bridges\ApplicationLatte\SnippetBridge($this)); - $latte->onCompile[] = function ($latte) { - $latte->getCompiler()->getMacros()['snippet'][0]->snippetAttribute = 'data-snippet'; - }; - $latte->render($template); - } -} - - -$presenter = new TestPresenter; -ob_start(); -$presenter->render('
    hello
    '); -$content = ob_get_clean(); -Assert::same('
    hello
    ', $content); - - -$presenter = new TestPresenter; -Assert::exception(function () use ($presenter) { - $presenter->render('
    hello
    '); -}, Latte\CompileException::class, 'Cannot combine HTML attribute data-snippet with n:snippet.'); diff --git a/tests/Bridges.Latte2/expected/UIMacros.isLinkCurrent.php b/tests/Bridges.Latte2/expected/UIMacros.isLinkCurrent.php deleted file mode 100644 index 15ef390af..000000000 --- a/tests/Bridges.Latte2/expected/UIMacros.isLinkCurrent.php +++ /dev/null @@ -1,22 +0,0 @@ -%A%n:href before n:class - -n:href after n:class - -href before n:class - -href after n:class - -'; - if ($this->global->uiPresenter->getLastCreatedRequestFlag("current")) { - echo 'empty'; - } - echo ' - -'; - if ($this->global->uiPresenter->isLinkCurrent("default")) { - echo 'default'; - } - echo ' - -global->fn->isLinkCurrent)('default') ? 'current' : null%A%>custom function -%A% diff --git a/tests/Bridges.Latte2/expected/UIMacros.renderSnippets.html b/tests/Bridges.Latte2/expected/UIMacros.renderSnippets.html deleted file mode 100644 index 3ff3683ed..000000000 --- a/tests/Bridges.Latte2/expected/UIMacros.renderSnippets.html +++ /dev/null @@ -1,20 +0,0 @@ -
    Hello
    world! - -

    Included file #3 (A, B)

    -
    - -
    Value 1
    -
    Value 2
    -
    Value 3
    -
    - -
    Value 1
    -
    Value 2
    -
    Value 3
    - - -
    Hello include snippet
    - - -
    Foo
    -
    diff --git a/tests/Bridges.Latte3/TemplateFactory.onCreate.phpt b/tests/Bridges.Latte3/TemplateFactory.onCreate.phpt deleted file mode 100644 index ed6e9b61c..000000000 --- a/tests/Bridges.Latte3/TemplateFactory.onCreate.phpt +++ /dev/null @@ -1,36 +0,0 @@ -shouldReceive('create')->andReturn($engine); - $factory = new TemplateFactory($latteFactory, new Http\Request(new Http\UrlScript('http://nette.org'))); - $factory->onCreate[] = $callback = function (Template $template) { - $template->add('foo', 'bar'); - }; - - $template = $factory->createTemplate(); - - Assert::type('array', $factory->onCreate); - Assert::same($callback, $factory->onCreate[0]); // our callback - Assert::equal('bar', $template->foo); -}); diff --git a/tests/Bridges.Latte3/expected/isLinkCurrent.php b/tests/Bridges.Latte3/expected/isLinkCurrent.php deleted file mode 100644 index bfb0c72d3..000000000 --- a/tests/Bridges.Latte3/expected/isLinkCurrent.php +++ /dev/null @@ -1,32 +0,0 @@ -%A% - echo 'isLinkCurrent() ? 'current' : null])) ? ' class="' . LR\%a%Attr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* %a% */; - echo '>n:href before n:class - -isLinkCurrent() ? 'current' : null])) ? ' class="' . LR\%a%Attr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* %a% */; - echo '>n:href after n:class - -isLinkCurrent() ? 'current' : null])) ? ' class="' . LR\%a%Attr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* %a% */; - echo '>href before n:class - -isLinkCurrent() ? 'current' : null])) ? ' class="' . LR\%a%Attr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* %a% */; - echo ' href="'; - echo LR\%a%Attr($this->global->uiControl->link('default')) /* %a% */; - echo '">href after n:class - -global->fn->isLinkCurrent)(%a%'default') ? 'current' : null])) ? ' class="' . LR\%a%Attr(implode(" ", array_unique($ʟ_tmp))) . '"' : "" /* %a% */; - echo '>custom function -'; -%A% diff --git a/tests/Bridges.Latte3/templates/include3.latte b/tests/Bridges.Latte3/templates/include3.latte deleted file mode 100644 index 14caf4cb4..000000000 --- a/tests/Bridges.Latte3/templates/include3.latte +++ /dev/null @@ -1 +0,0 @@ -

    Included file #3 ({$localvar}, {$hello})

    diff --git a/tests/Bridges.Latte3/templates/snippet-include.latte b/tests/Bridges.Latte3/templates/snippet-include.latte deleted file mode 100644 index 8a1ec964d..000000000 --- a/tests/Bridges.Latte3/templates/snippet-include.latte +++ /dev/null @@ -1,25 +0,0 @@ -{snippet hello}Hello{/snippet} world! - -{snippet include} - {include include3.latte localvar => 'A', hello => 'B'} -{/snippet} - -{snippet array} - {foreach [1, 2, 3] as $id} - {snippet "array-$id"}Value {$id}{/snippet} - {/foreach} -{/snippet} - -{snippetArea array2} - {foreach [1, 2, 3] as $id} - {snippet "array2-$id"}Value {$id}{/snippet} - {/foreach} -{/snippetArea} - -{snippetArea foo} - {include snippet-included.latte say => 'Hello include snippet'} -{/snippetArea} - -{snippet nested1} - {snippet nested2}Foo{/snippet} -{/snippet} diff --git a/tests/Bridges.Latte3/templates/snippet-included.latte b/tests/Bridges.Latte3/templates/snippet-included.latte deleted file mode 100644 index a392b6227..000000000 --- a/tests/Bridges.Latte3/templates/snippet-included.latte +++ /dev/null @@ -1 +0,0 @@ -{snippet includeSay}{$say}{/snippet} diff --git a/tests/Bridges.Latte3/{ifCurrent}.phpt b/tests/Bridges.Latte3/{ifCurrent}.phpt deleted file mode 100644 index 3c9ee133e..000000000 --- a/tests/Bridges.Latte3/{ifCurrent}.phpt +++ /dev/null @@ -1,35 +0,0 @@ -shouldReceive('getPresenterIfExists')->andReturn($presenter); -$presenter->shouldReceive('getHttpResponse')->andReturn((Mockery::mock(Nette\Http\IResponse::class))->shouldIgnoreMissing()); -$presenter->shouldIgnoreMissing(); - -$latte = new Latte\Engine; -$latte->setLoader(new Latte\Loaders\StringLoader); -$latte->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension($presenter)); - -Assert::matchFile( - __DIR__ . '/expected/ifCurrent.php', - @$latte->compile( // is deprecated - <<<'XX' - {ifCurrent}empty{/ifCurrent} - - {ifCurrent default}default{/ifCurrent} - XX, - ), -); diff --git a/tests/Routers/LinkGenerator.phpt b/tests/Routers/LinkGenerator.phpt index d82ea37a8..d211d9c14 100644 --- a/tests/Routers/LinkGenerator.phpt +++ b/tests/Routers/LinkGenerator.phpt @@ -6,7 +6,9 @@ declare(strict_types=1); -namespace { +namespace App\Presentation\Homepage { + + use Nette; require __DIR__ . '/../bootstrap.php'; @@ -24,7 +26,7 @@ namespace { } -namespace ModuleModule { +namespace App\Presentation\Module\My { use Nette; @@ -77,7 +79,7 @@ namespace { testException('invalid action parameter propagation', function () use ($pf) { $generator = new LinkGenerator(new Routers\Route('/', 'Homepage:'), new Http\UrlScript('http://nette.org/en/'), $pf); $generator->link('Homepage:missing', [10]); - }, Nette\Application\UI\InvalidLinkException::class, "Unable to pass parameters to action 'Homepage:missing', missing corresponding method HomepagePresenter::renderMissing()."); + }, Nette\Application\UI\InvalidLinkException::class, "Unable to pass parameters to action 'Homepage:missing', missing corresponding method App\\Presentation\\Homepage\\HomepagePresenter::renderMissing()."); test('URL generation without PresenterFactory', function () { diff --git a/tests/Routers/RouteList.addRoute.phpt b/tests/Routers/RouteList.addRoute.phpt index fd6ab423a..068f66805 100644 --- a/tests/Routers/RouteList.addRoute.phpt +++ b/tests/Routers/RouteList.addRoute.phpt @@ -12,7 +12,7 @@ require __DIR__ . '/Route.php'; $list = new RouteList; $list->addRoute('foo', ['presenter' => 'foo'], RouteList::ONE_WAY); -$list->addRoute('bar', ['presenter' => 'bar'], RouteList::ONE_WAY); +$list->addRoute('bar', ['presenter' => 'bar'], oneWay: true); $list->addRoute('hello', ['presenter' => 'hello']); diff --git a/tests/Routers/RouteList.basic.phpt b/tests/Routers/RouteList.basic.phpt index 22afeda6c..b4783db95 100644 --- a/tests/Routers/RouteList.basic.phpt +++ b/tests/Routers/RouteList.basic.phpt @@ -6,7 +6,6 @@ declare(strict_types=1); -use Nette\Application\Routers\Route; use Nette\Application\Routers\RouteList; use Tester\Assert; @@ -17,7 +16,7 @@ require __DIR__ . '/Route.php'; $list = new RouteList; -$list[] = new Route('//'); +$list->addRoute('//'); Assert::same('http://example.com/front.homepage/', testRouteOut($list, ['presenter' => 'Front:Homepage'])); diff --git a/tests/Routers/RouteList.modules.phpt b/tests/Routers/RouteList.modules.phpt index db2aff0d2..ff1e15193 100644 --- a/tests/Routers/RouteList.modules.phpt +++ b/tests/Routers/RouteList.modules.phpt @@ -6,7 +6,6 @@ declare(strict_types=1); -use Nette\Application\Routers\Route; use Nette\Application\Routers\RouteList; @@ -16,12 +15,12 @@ require __DIR__ . '/Route.php'; $list = new RouteList; -$list[] = new Route('auth/[/]', [ +$list->addRoute('auth/[/]', [ 'module' => 'Auth', 'presenter' => 'Homepage', 'action' => 'default', ]); -$list[] = new Route('[/]', [ +$list->addRoute('[/]', [ 'module' => 'Default', 'presenter' => 'Homepage', 'action' => 'default', diff --git a/tests/Routers/link-aliases.phpt b/tests/Routers/link-aliases.phpt index 0f2b75324..2180bd526 100644 --- a/tests/Routers/link-aliases.phpt +++ b/tests/Routers/link-aliases.phpt @@ -20,6 +20,7 @@ class TestPresenter extends Application\UI\Presenter $factory = new PresenterFactory; +$factory->setMapping(['*' => '*Presenter']); $factory->setAliases([ 'a' => 'Test:a', 'b' => 'Test:b', diff --git a/tests/UI/Component.redirect().phpt b/tests/UI/Component.redirect().phpt index e60a89939..3afc6a9a7 100644 --- a/tests/UI/Component.redirect().phpt +++ b/tests/UI/Component.redirect().phpt @@ -24,7 +24,7 @@ class TestPresenter extends Application\UI\Presenter } - public function sendResponse(Application\Response $response): void + public function sendResponse(Application\Response $response): never { parent::sendResponse($this->response = $response); } diff --git a/tests/UI/Component.signals.phpt b/tests/UI/Component.signals.phpt new file mode 100644 index 000000000..5a96b87a3 --- /dev/null +++ b/tests/UI/Component.signals.phpt @@ -0,0 +1,174 @@ +signalCalled = true; + } +} + + +class ParentControl extends Application\UI\Control +{ + public bool $signalCalled = false; + + + public function handleUpdate(): void + { + $this->signalCalled = true; + } + + + protected function createComponentNested(): NestedControl + { + return new NestedControl; + } +} + + +test('signal routing to nested component', function () { + $presenter = new class extends Application\UI\Presenter { + protected function createComponentParent(): ParentControl + { + return new ParentControl; + } + + + public function renderDefault(): void + { + $this->terminate(); + } + }; + + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript, cookies: [Http\Helpers::StrictCookieName => 1]), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', [ + 'action' => 'default', + 'do' => 'parent-nested-refresh', + ])); + + $parent = $presenter['parent']; + $nested = $parent['nested']; + + Assert::type(ParentControl::class, $parent); + Assert::type(NestedControl::class, $nested); + Assert::false($parent->signalCalled); + Assert::true($nested->signalCalled); +}); + + +class ParameterControl extends Application\UI\Control +{ + public ?int $receivedParam = null; + + + public function handleClick(int $id): void + { + $this->receivedParam = $id; + } +} + + +test('signal with parameters', function () { + $presenter = new class extends Application\UI\Presenter { + protected function createComponentParam(): ParameterControl + { + return new ParameterControl; + } + + + public function renderDefault(): void + { + $this->terminate(); + } + }; + + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript, cookies: [Http\Helpers::StrictCookieName => 1]), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', [ + 'action' => 'default', + 'do' => 'param-click', + 'param-id' => '42', + ])); + + $control = $presenter['param']; + + Assert::type(ParameterControl::class, $control); + Assert::same(42, $control->receivedParam); +}); + + +testException('invalid signal name throws exception', function () { + $presenter = new class extends Application\UI\Presenter { + public function renderDefault(): void + { + $this->terminate(); + } + }; + + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript, cookies: [Http\Helpers::StrictCookieName => 1]), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', [ + 'action' => 'default', + 'do' => 'nonexistent', + ])); +}, Application\UI\BadSignalException::class); + + +testException('signal to nonexistent component throws exception', function () { + $presenter = new class extends Application\UI\Presenter { + public function renderDefault(): void + { + $this->terminate(); + } + }; + + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript, cookies: [Http\Helpers::StrictCookieName => 1]), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', [ + 'action' => 'default', + 'do' => 'nonexistent-click', + ])); +}, Application\UI\BadSignalException::class); diff --git a/tests/UI/ComponentReflection.getParameters().phpt b/tests/UI/ComponentReflection.getParameters().phpt index 5567ab142..ca70724af 100644 --- a/tests/UI/ComponentReflection.getParameters().phpt +++ b/tests/UI/ComponentReflection.getParameters().phpt @@ -16,7 +16,7 @@ class OnePresenter extends Presenter public static $no1; public $no2; - /** @persistent */ + #[Persistent] public $yes1; #[Persistent, Parameter] diff --git a/tests/UI/ComponentReflection.getPersistentComponents().phpt b/tests/UI/ComponentReflection.getPersistentComponents().phpt index 2654e06d9..febe683f3 100644 --- a/tests/UI/ComponentReflection.getPersistentComponents().phpt +++ b/tests/UI/ComponentReflection.getPersistentComponents().phpt @@ -14,14 +14,6 @@ class NonePresenter extends Presenter } -/** - * @persistent(a, b) - */ -class AnnotationPresenter extends Presenter -{ -} - - #[Persistent('a', 'b')] class AttributePresenter extends Presenter { @@ -39,11 +31,6 @@ class MethodPresenter extends AttributePresenter Assert::same([], NonePresenter::getReflection()->getPersistentComponents()); -Assert::same([ - 'a' => ['since' => AnnotationPresenter::class], - 'b' => ['since' => AnnotationPresenter::class], -], AnnotationPresenter::getReflection()->getPersistentComponents()); - Assert::same([ 'a' => ['since' => AttributePresenter::class], 'b' => ['since' => AttributePresenter::class], diff --git a/tests/UI/ComponentReflection.parseAnnotation.phpt b/tests/UI/ComponentReflection.parseAnnotation.phpt deleted file mode 100644 index 278b5e7d1..000000000 --- a/tests/UI/ComponentReflection.parseAnnotation.phpt +++ /dev/null @@ -1,44 +0,0 @@ - false, 'args' => [], 'fragment' => '#fragment', -], LinkGenerator::parseDestination('a:b?#fragment')); +], @LinkGenerator::parseDestination('a:b?#fragment')); // deprecated Assert::same([ 'absolute' => false, @@ -56,7 +56,7 @@ Assert::same([ 'signal' => false, 'args' => ['a' => 'b', 'c' => 'd'], 'fragment' => '#fragment', -], LinkGenerator::parseDestination('a:b?a=b&c=d#fragment')); +], @LinkGenerator::parseDestination('a:b?a=b&c=d#fragment')); // deprecated Assert::exception( fn() => LinkGenerator::parseDestination(''), diff --git a/tests/UI/Multiplier.phpt b/tests/UI/Multiplier.phpt new file mode 100644 index 000000000..6838b91d9 --- /dev/null +++ b/tests/UI/Multiplier.phpt @@ -0,0 +1,77 @@ +getComponent('item1'); + $component2 = $multiplier->getComponent('item2'); + + Assert::type(TestControl::class, $component1); + Assert::type(TestControl::class, $component2); + Assert::same('item1', $component1->id); + Assert::same('item2', $component2->id); + Assert::same(['item1', 'item2'], $calls); +}); + + +test('same name returns cached component', function () { + $calls = 0; + $multiplier = new Application\UI\Multiplier(function ($name) use (&$calls) { + $calls++; + return new TestControl($name); + }); + + $component1 = $multiplier->getComponent('item1'); + $component2 = $multiplier->getComponent('item1'); + + Assert::same($component1, $component2); + Assert::same(1, $calls); +}); + + +test('factory receives component name and parent', function () { + $receivedName = null; + $receivedParent = null; + + $multiplier = new Application\UI\Multiplier(function ($name, $parent) use (&$receivedName, &$receivedParent) { + $receivedName = $name; + $receivedParent = $parent; + return new TestControl($name); + }); + + $multiplier->getComponent('test'); + + Assert::same('test', $receivedName); + Assert::same($multiplier, $receivedParent); +}); diff --git a/tests/UI/Presenter.canonicalize.phpt b/tests/UI/Presenter.canonicalize.phpt new file mode 100644 index 000000000..306f873ba --- /dev/null +++ b/tests/UI/Presenter.canonicalize.phpt @@ -0,0 +1,230 @@ +terminate(); + } +} + + +test('autoCanonicalize redirects to remove default persistent parameter', function () { + $presenter = new CanonicalPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript('http://localhost/index.php?id=0&presenter=Canonical&action=default')), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = true; + + $response = $presenter->run(new Application\Request('Canonical', 'GET', [ + 'action' => 'default', + 'id' => 0, + ])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + Assert::notContains('id=0', $response->getUrl()); +}); + + +test('autoCanonicalize disabled does not redirect', function () { + $presenter = new CanonicalPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript('http://localhost/index.php?extra=param&id=5&presenter=Canonical&action=default')), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + + $response = $presenter->run(new Application\Request('Canonical', 'GET', [ + 'action' => 'default', + 'id' => 5, + ])); + + Assert::type(Application\Responses\VoidResponse::class, $response); +}); + + +test('canonicalize() does not redirect on AJAX request', function () { + $presenter = createPresenter(CanonicalPresenter::class, headers: ['X-Requested-With' => 'XMLHttpRequest']); + $presenter->autoCanonicalize = true; + + $response = $presenter->run(new Application\Request('Canonical', 'GET', [ + 'action' => 'default', + 'id' => 5, + ])); + + Assert::type(Application\Responses\VoidResponse::class, $response); +}); + + +test('canonicalize() does not redirect on POST request', function () { + $presenter = createPresenter(CanonicalPresenter::class, post: ['data' => 'value']); + $presenter->autoCanonicalize = true; + + $response = $presenter->run(new Application\Request('Canonical', 'POST', [ + 'action' => 'default', + 'id' => 5, + ])); + + Assert::type(Application\Responses\VoidResponse::class, $response); +}); + + +class ManualCanonicalPresenter extends Application\UI\Presenter +{ + public function actionDefault(): void + { + $this->canonicalize(); + } + + + public function renderDefault(): void + { + $this->terminate(); + } +} + + +test('manual canonicalize call works', function () { + $presenter = new ManualCanonicalPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript('http://localhost/index.php?extra=param&presenter=ManualCanonical&action=default')), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + + $response = $presenter->run(new Application\Request('ManualCanonical', 'GET', [ + 'action' => 'default', + ])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); +}); + + +class VaryingPresenter extends Application\UI\Presenter +{ + public function actionDefault(): void + { + } + + + public function renderDefault(): void + { + $this->terminate(); + } +} + + +test('canonicalize() uses 301 for non-varying requests', function () { + $presenter = new VaryingPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript('http://localhost/index.php?extra=param&presenter=Varying&action=default')), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = true; + + $response = $presenter->run(new Application\Request('Varying', 'GET', [ + 'action' => 'default', + ])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + Assert::same(Http\IResponse::S301_MovedPermanently, $response->getCode()); +}); + + +test('canonicalize() uses 302 for varying requests', function () { + $presenter = new VaryingPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript('http://localhost/index.php?extra=param&presenter=Varying&action=default')), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = true; + + $request = new Application\Request('Varying', 'GET', ['action' => 'default']); + $request->setFlag($request::VARYING, true); + + $response = $presenter->run($request); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + Assert::same(Http\IResponse::S302_Found, $response->getCode()); +}); + + +class DestinationCanonicalPresenter extends Application\UI\Presenter +{ + public function actionDefault(): void + { + $this->canonicalize('other'); + } + + + public function actionOther(): void + { + } + + + public function renderDefault(): void + { + $this->terminate(); + } + + + public function renderOther(): void + { + $this->terminate(); + } +} + + +test('canonicalize() with different destination redirects', function () { + $presenter = new DestinationCanonicalPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript('http://localhost/index.php?presenter=DestinationCanonical&action=default')), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + + $response = $presenter->run(new Application\Request('DestinationCanonical', 'GET', [ + 'action' => 'default', + ])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + Assert::contains('action=other', $response->getUrl()); +}); diff --git a/tests/UI/Presenter.flashMessage.phpt b/tests/UI/Presenter.flashMessage.phpt new file mode 100644 index 000000000..9f5bee7da --- /dev/null +++ b/tests/UI/Presenter.flashMessage.phpt @@ -0,0 +1,257 @@ +terminate(); + } +} + + +class FlashPresenter extends Application\UI\Presenter +{ + public function actionDefault() + { + $this->flashMessage('Test message'); + } + + + public function renderDefault() + { + $this->terminate(); + } +} + + +test('flash message is stored in session', function () { + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->andReturn([]); + $sessionSection->shouldReceive('set')->once(); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + $session->shouldReceive('hasSection')->andReturn(true); + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + session: $session, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + + $flash = $presenter->flashMessage('Test message'); + Assert::type(stdClass::class, $flash); + Assert::same('Test message', $flash->message); + Assert::same('info', $flash->type); +}); + + +test('flash message with custom type', function () { + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->andReturn([]); + $sessionSection->shouldReceive('set')->once(); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + $session->shouldReceive('hasSection')->andReturn(true); + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + session: $session, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + + $flash = $presenter->flashMessage('Error occurred', 'error'); + Assert::same('Error occurred', $flash->message); + Assert::same('error', $flash->type); +}); + + +test('multiple flash messages are stored', function () { + $messages = []; + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->andReturnUsing(function () use (&$messages) { + return $messages; + }); + $sessionSection->shouldReceive('set')->andReturnUsing(function ($id, $value) use (&$messages) { + $messages = $value; + }); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + $session->shouldReceive('hasSection')->andReturn(true); + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + session: $session, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + + $presenter->flashMessage('First message', 'info'); + $presenter->flashMessage('Second message', 'error'); + $presenter->flashMessage('Third message', 'success'); + + Assert::count(3, $messages); + Assert::same('First message', $messages[0]->message); + Assert::same('info', $messages[0]->type); + Assert::same('Second message', $messages[1]->message); + Assert::same('error', $messages[1]->type); + Assert::same('Third message', $messages[2]->message); + Assert::same('success', $messages[2]->type); +}); + + +test('flash message is available in template', function () { + $messages = []; + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->andReturnUsing(function () use (&$messages) { + return $messages; + }); + $sessionSection->shouldReceive('set')->andReturnUsing(function ($id, $value) use (&$messages) { + $messages = $value; + }); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + $session->shouldReceive('hasSection')->andReturn(true); + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + session: $session, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + + $presenter->flashMessage('Template message'); + + $template = $presenter->getTemplate(); + Assert::count(1, $template->flashes); + Assert::same('Template message', $template->flashes[0]->message); +}); + + +test('flash session expires after presenter run', function () { + $expirationSet = false; + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->andReturn([]); + $sessionSection->shouldReceive('set')->once(); + $sessionSection->shouldReceive('setExpiration')->once()->with('30 seconds')->andReturnUsing(function () use (&$expirationSet, $sessionSection) { + $expirationSet = true; + return $sessionSection; + }); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + $session->shouldReceive('hasSection')->andReturn(true); + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new FlashPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + session: $session, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Flash', 'GET', ['action' => 'default'])); + + Assert::true($expirationSet); +}); + + +test('flash message accepts stdClass object', function () { + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->andReturn([]); + $sessionSection->shouldReceive('set')->once(); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + $session->shouldReceive('hasSection')->andReturn(true); + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + session: $session, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + + $customFlash = (object) [ + 'message' => 'Custom message', + 'type' => 'warning', + 'custom' => 'data', + ]; + $flash = $presenter->flashMessage($customFlash); + + Assert::same($customFlash, $flash); + Assert::same('Custom message', $flash->message); + Assert::same('warning', $flash->type); + Assert::same('data', $flash->custom); +}); diff --git a/tests/UI/Presenter.lifecycle.phpt b/tests/UI/Presenter.lifecycle.phpt new file mode 100644 index 000000000..c3cc21ab4 --- /dev/null +++ b/tests/UI/Presenter.lifecycle.phpt @@ -0,0 +1,248 @@ +called[] = 'startup'; + } + + + public function actionDefault(): void + { + $this->called[] = 'actionDefault'; + } + + + protected function beforeRender(): void + { + parent::beforeRender(); + $this->called[] = 'beforeRender'; + } + + + public function renderDefault(): void + { + $this->called[] = 'renderDefault'; + $this->terminate(); + } + + + protected function afterRender(): void + { + parent::afterRender(); + $this->called[] = 'afterRender'; + } + + + protected function shutdown(Application\Response $response): void + { + parent::shutdown($response); + $this->called[] = 'shutdown'; + } +} + + +test('lifecycle methods are called in correct order', function () { + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new LifecyclePresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Lifecycle', 'GET', ['action' => 'default'])); + + Assert::same( + ['startup', 'actionDefault', 'beforeRender', 'renderDefault', 'shutdown'], + $presenter->called, + ); +}); + + +test('onStartup event is fired before startup', function () { + $eventCalled = false; + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new LifecyclePresenter; + $presenter->onStartup[] = function () use (&$eventCalled, $presenter) { + $eventCalled = true; + Assert::same([], $presenter->called); + }; + + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Lifecycle', 'GET', ['action' => 'default'])); + + Assert::true($eventCalled); +}); + + +test('onRender event is fired after beforeRender', function () { + $eventCalled = false; + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new LifecyclePresenter; + $presenter->onRender[] = function () use (&$eventCalled, $presenter) { + $eventCalled = true; + Assert::same(['startup', 'actionDefault', 'beforeRender'], $presenter->called); + }; + + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Lifecycle', 'GET', ['action' => 'default'])); + + Assert::true($eventCalled); +}); + + +test('onShutdown event is fired before shutdown', function () { + $eventCalled = false; + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $templateFactory = Mockery::mock(Application\UI\TemplateFactory::class); + $templateFactory->shouldReceive('createTemplate')->andReturn(new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte)); + + $presenter = new LifecyclePresenter; + $presenter->onShutdown[] = function () use (&$eventCalled, $presenter) { + $eventCalled = true; + Assert::same(['startup', 'actionDefault', 'beforeRender', 'renderDefault'], $presenter->called); + }; + + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Lifecycle', 'GET', ['action' => 'default'])); + + Assert::true($eventCalled); +}); + + +class SignalPresenter extends Application\UI\Presenter +{ + public array $called = []; + + + protected function startup(): void + { + parent::startup(); + $this->called[] = 'startup'; + } + + + public function handleRefresh(): void + { + $this->called[] = 'handleRefresh'; + } + + + public function actionDefault(): void + { + $this->called[] = 'actionDefault'; + } + + + public function renderDefault(): void + { + $this->called[] = 'renderDefault'; + $this->terminate(); + } +} + + +test('signal is called after action', function () { + $presenter = createPresenter(SignalPresenter::class, cookies: [Http\Helpers::StrictCookieName => 1]); + + $presenter->run(new Application\Request('Signal', 'GET', [ + 'action' => 'default', + 'do' => 'refresh', + ])); + + Assert::same(['startup', 'actionDefault', 'handleRefresh', 'renderDefault'], $presenter->called); +}); + + +class RedirectPresenter extends Application\UI\Presenter +{ + public array $called = []; + + + public function actionDefault(): void + { + $this->called[] = 'actionDefault'; + $this->redirect('other'); + } + + + public function actionOther(): void + { + $this->called[] = 'actionOther'; + } + + + public function renderOther(): void + { + $this->called[] = 'renderOther'; + $this->terminate(); + } +} + + +test('redirect terminates current lifecycle', function () { + $presenter = createPresenter(RedirectPresenter::class); + + $response = $presenter->run(new Application\Request('Redirect', 'GET', ['action' => 'default'])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + Assert::same(['actionDefault'], $presenter->called); + Assert::notContains('renderDefault', $presenter->called); + Assert::notContains('renderOther', $presenter->called); +}); diff --git a/tests/UI/Presenter.link().persistent.phpt b/tests/UI/Presenter.link().persistent.phpt index 548f122f2..f181adce7 100644 --- a/tests/UI/Presenter.link().persistent.phpt +++ b/tests/UI/Presenter.link().persistent.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\Application; +use Nette\Application\Attributes\Persistent; use Nette\Http; use Tester\Assert; @@ -23,13 +24,13 @@ function sortParams(array $params): array trait PersistentParam1 { - /** @persistent */ + #[Persistent] public $t1; } trait PersistentParam2A { - /** @persistent */ + #[Persistent] public $t2; } @@ -40,7 +41,7 @@ trait PersistentParam2B trait PersistentParam3 { - /** @persistent */ + #[Persistent] public $t3; } @@ -48,7 +49,7 @@ class BasePresenter extends Application\UI\Presenter { use PersistentParam1; - /** @persistent */ + #[Persistent] public $p1; } @@ -57,7 +58,7 @@ class TestPresenter extends BasePresenter { use PersistentParam2B; - /** @persistent */ + #[Persistent] public $p2; @@ -92,10 +93,10 @@ class SecondPresenter extends BasePresenter { use PersistentParam3; - /** @persistent */ + #[Persistent] public $p1 = 20; - /** @persistent */ + #[Persistent] public $p3; } @@ -108,7 +109,7 @@ class ThirdPresenter extends BasePresenter class FourthPresenter extends BasePresenter { - #[Application\Attributes\Persistent] + #[Persistent] public $p1; } diff --git a/tests/UI/Presenter.link().phpt b/tests/UI/Presenter.link().phpt index b6b129d28..38407c6dd 100644 --- a/tests/UI/Presenter.link().phpt +++ b/tests/UI/Presenter.link().phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\Application; +use Nette\Application\Attributes\Persistent; use Nette\Http; use Tester\Assert; @@ -16,10 +17,10 @@ require __DIR__ . '/../bootstrap.php'; class TestControl extends Application\UI\Control { - /** @persistent array */ + #[Persistent] public $order = []; - /** @persistent int */ + #[Persistent] public $round = 0; @@ -50,22 +51,22 @@ class TestControl extends Application\UI\Control class TestPresenter extends Application\UI\Presenter { - /** @persistent */ + #[Persistent] public $p; - /** @persistent */ + #[Persistent] public $pint = 10; - /** @persistent */ + #[Persistent] public $parr = []; - /** @persistent */ + #[Persistent] public $pbool = true; - /** @persistent */ + #[Persistent] public array $parrn; - /** @persistent */ + #[Persistent] public ?bool $pbooln = null; @@ -90,15 +91,11 @@ class TestPresenter extends Application\UI\Presenter Assert::same('/index.php?a=1&action=product&presenter=Test', $this->link('product', ['a' => 1])); Assert::same('#error: Passed more parameters than method TestPresenter::actionParams() expects.', $this->link('params', 1, 2, 3, 4, 5)); - // special url - Assert::same('/index.php?x=1&y=2&action=product&presenter=Test', $this->link('product?x=1&y=2')); - Assert::same('/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('product?x=1&y=2#fragment')); - // absolute - Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('//product?x=1&y=2#fragment')); + Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('//product#fragment', ['x' => 1, 'y' => 2])); $this->absoluteUrls = true; - Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('product?x=1&y=2#fragment')); - Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('//product?x=1&y=2#fragment')); + Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('product#fragment', ['x' => 1, 'y' => 2])); + Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('//product#fragment', ['x' => 1, 'y' => 2])); $this->absoluteUrls = false; // persistent params @@ -142,7 +139,7 @@ class TestPresenter extends Application\UI\Presenter Assert::same('/index.php?mycontrol-x=1&mycontrol-y=0&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', true, false)); Assert::same('/index.php?action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', null, '')); Assert::same('#error: Passed more parameters than method TestControl::handleClick() expects.', $this['mycontrol']->link('click', 1, 2, 3)); - Assert::same('http://localhost/index.php?mycontrol-x=1&mycontrol-round=1&action=default&presenter=Test#frag', $this['mycontrol']->link('//this?x=1&round=1#frag')); + Assert::same('http://localhost/index.php?mycontrol-x=1&mycontrol-round=1&action=default&presenter=Test#frag', $this['mycontrol']->link('//this#frag', ['x' => 1, 'round' => 1])); Assert::same('/index.php?mycontrol-x=1&mycontrol-y=2&action=default&do=mycontrol-click&presenter=Test', $this->link('mycontrol:click!', ['x' => 1, 'y' => 2, 'round' => 0])); Assert::same(['mycontrol-x' => 1, 'mycontrol-y' => 2, 'mycontrol-round' => null, 'mycontrol-order' => null, 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'default', 'do' => 'mycontrol-click'], $this->getLastCreatedRequest()->getParameters()); @@ -294,7 +291,7 @@ class TestPresenter extends Application\UI\Presenter class OtherPresenter extends TestPresenter { - /** @persistent */ + #[Persistent] public $p = 20; } diff --git a/tests/UI/Presenter.redirects.phpt b/tests/UI/Presenter.redirects.phpt new file mode 100644 index 000000000..dd6eaef32 --- /dev/null +++ b/tests/UI/Presenter.redirects.phpt @@ -0,0 +1,241 @@ +redirect('other', ['page' => 2]); + } + + + public function actionOther(): void + { + $this->terminate(); + } +} + + +test('redirect() creates RedirectResponse with 302 code', function () { + $presenter = createPresenter(RedirectPresenter::class); + + $response = $presenter->run(new Application\Request('Redirect', 'GET', ['action' => 'default'])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + Assert::same(Http\IResponse::S302_Found, $response->getCode()); +}); + + +class PermanentRedirectPresenter extends Application\UI\Presenter +{ + public function actionDefault(): void + { + $this->redirectPermanent('other'); + } + + + public function actionOther(): void + { + $this->terminate(); + } +} + + +test('redirectPermanent() creates RedirectResponse with 301 code', function () { + $presenter = createPresenter(PermanentRedirectPresenter::class); + + $response = $presenter->run(new Application\Request('PermanentRedirect', 'GET', ['action' => 'default'])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + Assert::same(Http\IResponse::S301_MovedPermanently, $response->getCode()); +}); + + +class ForwardPresenter extends Application\UI\Presenter +{ + public function actionDefault(): void + { + $this->forward('other'); + } + + + public function actionOther(): void + { + $this->terminate(); + } +} + + +test('forward() creates ForwardResponse', function () { + $presenter = createPresenter(ForwardPresenter::class); + + $response = $presenter->run(new Application\Request('Forward', 'GET', ['action' => 'default'])); + + Assert::type(Application\Responses\ForwardResponse::class, $response); +}); + + +class PersistentParamsPresenter extends Application\UI\Presenter +{ + #[Persistent] + public int $page = 1; + + #[Persistent] + public string $lang = 'en'; + + + public function actionDefault(): void + { + $this->redirect('other'); + } + + + public function actionOther(): void + { + $this->terminate(); + } +} + + +test('redirect() preserves persistent parameters', function () { + $presenter = new PersistentParamsPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript('http://localhost')), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + + $response = $presenter->run(new Application\Request('PersistentParams', 'GET', [ + 'action' => 'default', + 'page' => 5, + 'lang' => 'cs', + ])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + $url = $response->getUrl(); + Assert::contains('page=5', $url); + Assert::contains('lang=cs', $url); +}); + + +class OverrideParamsPresenter extends Application\UI\Presenter +{ + #[Persistent] + public int $page = 1; + + #[Persistent] + public string $lang = 'en'; + + + public function actionDefault(): void + { + $this->redirect('other', ['page' => 10, 'lang' => 'de']); + } + + + public function actionOther(): void + { + $this->terminate(); + } +} + + +test('redirect() can override persistent parameters', function () { + $presenter = new OverrideParamsPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript('http://localhost')), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + + $response = $presenter->run(new Application\Request('OverrideParams', 'GET', [ + 'action' => 'default', + 'page' => 5, + 'lang' => 'cs', + ])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + $url = $response->getUrl(); + Assert::contains('page=10', $url); + Assert::contains('lang=de', $url); +}); + + +class UrlRedirectPresenter extends Application\UI\Presenter +{ + public function actionDefault(): void + { + $this->redirectUrl('https://example.com/path'); + } +} + + +test('redirectUrl() creates RedirectResponse to external URL', function () { + $presenter = new UrlRedirectPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript('http://localhost')), + new Http\Response, + ); + $presenter->autoCanonicalize = false; + + $response = $presenter->run(new Application\Request('UrlRedirect', 'GET', ['action' => 'default'])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + Assert::same('https://example.com/path', $response->getUrl()); +}); + + +class PostRedirectPresenter extends Application\UI\Presenter +{ + public function actionDefault(): void + { + if ($this->getRequest()->getMethod() === 'POST') { + $this->redirect('success'); + } + } + + + public function actionSuccess(): void + { + $this->terminate(); + } + + + public function renderDefault(): void + { + $this->terminate(); + } +} + + +test('POST request redirect uses 302 code', function () { + $presenter = createPresenter(PostRedirectPresenter::class, post: ['data' => 'value']); + + $response = $presenter->run(new Application\Request('PostRedirect', 'POST', ['action' => 'default'])); + + Assert::type(Application\Responses\RedirectResponse::class, $response); + Assert::same(Http\IResponse::S302_Found, $response->getCode()); +}); diff --git a/tests/UI/Presenter.storeRequest().phpt b/tests/UI/Presenter.storeRequest().phpt index 50ca0cd76..79b6ff802 100644 --- a/tests/UI/Presenter.storeRequest().phpt +++ b/tests/UI/Presenter.storeRequest().phpt @@ -128,3 +128,140 @@ test('request storage without user context', function () { Assert::same($key, $storedKey); Assert::same([null, $applicationRequest], $storedValue); }); + + +test('restoreRequest() restores stored request', function () { + $storedData = [ + 'test_id', + new Application\Request('Test', 'POST', ['action' => 'edit', 'id' => 5], ['name' => 'John']), + ]; + + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->with('abc123')->andReturn($storedData); + $sessionSection->shouldReceive('remove')->with('abc123')->once(); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + + $user = Mockery::mock(Nette\Security\User::class); + $user->shouldReceive('getId')->andReturn('test_id'); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + $session, + $user, + ); + + $presenter->autoCanonicalize = false; + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + + Assert::exception( + fn() => $presenter->restoreRequest('abc123'), + Application\AbortException::class, + ); + + // Verified that get/remove were called (via Mockery expectations) +}); + + +test('restoreRequest() ignores request with wrong user', function () { + $storedData = [ + 'different_user', + new Application\Request('Test', 'POST', ['action' => 'edit']), + ]; + + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->with('abc123')->andReturn($storedData); + $sessionSection->shouldReceive('remove')->never(); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + + $user = Mockery::mock(Nette\Security\User::class); + $user->shouldReceive('getId')->andReturn('test_id'); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + null, + new Application\Routers\SimpleRouter, + $session, + $user, + ); + + $presenter->autoCanonicalize = false; + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + + $presenter->restoreRequest('abc123'); + + // Request should not be restored due to user mismatch +}); + + +test('restoreRequest() handles missing request', function () { + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->with('nonexistent')->andReturn(null); + $sessionSection->shouldReceive('remove')->never(); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + null, + new Application\Routers\SimpleRouter, + $session, + ); + + $presenter->autoCanonicalize = false; + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + + $presenter->restoreRequest('nonexistent'); + + // Should handle gracefully without error +}); + + +test('restoreRequest() accepts request without user when stored without user', function () { + $storedData = [ + null, + new Application\Request('Test', 'POST', ['action' => 'edit']), + ]; + + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->with('abc123')->andReturn($storedData); + $sessionSection->shouldReceive('remove')->with('abc123')->once(); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + + $user = Mockery::mock(Nette\Security\User::class); + $user->shouldReceive('getId')->andReturn('test_id'); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + new Application\PresenterFactory, + new Application\Routers\SimpleRouter, + $session, + $user, + ); + + $presenter->autoCanonicalize = false; + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + + Assert::exception( + fn() => $presenter->restoreRequest('abc123'), + Application\AbortException::class, + ); + + // Request stored without user (null) should be restorable by any user +}); diff --git a/tests/UI/Requires.combinations.phpt b/tests/UI/Requires.combinations.phpt new file mode 100644 index 000000000..2bb4bba18 --- /dev/null +++ b/tests/UI/Requires.combinations.phpt @@ -0,0 +1,255 @@ +terminate(); + } +} + + +test('POST + AJAX requirement both satisfied', function () { + $presenter = createPresenter( + PostAjaxPresenter::class, + method: Http\Request::Post, + headers: ['X-Requested-With' => 'XMLHttpRequest'], + ); + + Assert::noError( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Post)), + ); +}); + + +test('POST requirement satisfied but AJAX missing', function () { + $presenter = createPresenter( + PostAjaxPresenter::class, + method: Http\Request::Post, + ); + + Assert::exception( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Post)), + Application\BadRequestException::class, + 'AJAX request is required by PostAjaxPresenter', + ); +}); + + +test('AJAX requirement satisfied but wrong HTTP method', function () { + $presenter = createPresenter( + PostAjaxPresenter::class, + headers: ['X-Requested-With' => 'XMLHttpRequest'], + ); + + Assert::exception( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Get)), + Application\BadRequestException::class, + 'Method GET is not allowed by PostAjaxPresenter', + ); +}); + + +#[Requires(sameOrigin: true, methods: 'POST')] +class CsrfPostPresenter extends Application\UI\Presenter +{ + public function actionDefault(): never + { + $this->terminate(); + } +} + + +test('sameOrigin + POST both satisfied', function () { + $presenter = createPresenter( + CsrfPostPresenter::class, + method: Http\Request::Post, + cookies: [Http\Helpers::StrictCookieName => 1], + ); + + Assert::noError( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Post)), + ); +}); + + +test('sameOrigin satisfied but wrong method', function () { + $presenter = createPresenter( + CsrfPostPresenter::class, + cookies: [Http\Helpers::StrictCookieName => 1], + ); + + Assert::exception( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Get)), + Application\BadRequestException::class, + 'Method GET is not allowed by CsrfPostPresenter', + ); +}); + + +test('POST satisfied but sameOrigin violated', function () { + $presenter = createPresenter( + CsrfPostPresenter::class, + method: Http\Request::Post, + ); + + // Without the strict cookie, request is considered cross-origin and redirected + $response = $presenter->run(new Application\Request('Test', Http\Request::Post)); + Assert::type(Application\Responses\RedirectResponse::class, $response); +}); + + +class MultiRequiresPresenter extends Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + #[Requires(ajax: true)] + public function actionDefault(): never + { + $this->terminate(); + } +} + + +test('multiple Requires attributes on same action', function () { + $presenter = createPresenter( + MultiRequiresPresenter::class, + method: Http\Request::Post, + headers: ['X-Requested-With' => 'XMLHttpRequest'], + ); + + Assert::noError( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Post, ['action' => 'default'])), + ); +}); + + +test('multiple Requires - first satisfied, second violated', function () { + $presenter = createPresenter( + MultiRequiresPresenter::class, + method: Http\Request::Post, + ); + + Assert::exception( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Post, ['action' => 'default'])), + Application\BadRequestException::class, + 'AJAX request is required by MultiRequiresPresenter::actionDefault()', + ); +}); + + +test('multiple Requires - second satisfied, first violated', function () { + $presenter = createPresenter( + MultiRequiresPresenter::class, + headers: ['X-Requested-With' => 'XMLHttpRequest'], + ); + + Assert::exception( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Get, ['action' => 'default'])), + Application\BadRequestException::class, + 'Method GET is not allowed by MultiRequiresPresenter::actionDefault()', + ); +}); + + +#[Requires(ajax: true)] +class AjaxPresenterWithAction extends Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionEdit(): never + { + $this->terminate(); + } + + + public function actionDefault(): never + { + $this->terminate(); + } +} + + +test('class-level + method-level requirements combine', function () { + $presenter = createPresenter( + AjaxPresenterWithAction::class, + method: Http\Request::Post, + headers: ['X-Requested-With' => 'XMLHttpRequest'], + ); + + Assert::noError( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Post, ['action' => 'edit'])), + ); +}); + + +test('class-level requirement satisfied, method-level violated', function () { + $presenter = createPresenter( + AjaxPresenterWithAction::class, + headers: ['X-Requested-With' => 'XMLHttpRequest'], + ); + + Assert::exception( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Get, ['action' => 'edit'])), + Application\BadRequestException::class, + 'Method GET is not allowed by AjaxPresenterWithAction::actionEdit()', + ); +}); + + +test('method-level requirement satisfied, class-level violated', function () { + $presenter = createPresenter( + AjaxPresenterWithAction::class, + method: Http\Request::Post, + ); + + Assert::exception( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Post, ['action' => 'edit'])), + Application\BadRequestException::class, + 'AJAX request is required by AjaxPresenterWithAction', + ); +}); + + +#[Requires(forward: true)] +class ForwardPresenter extends Application\UI\Presenter +{ + public function actionDefault(): never + { + $this->terminate(); + } +} + + +test('forward requirement satisfied', function () { + $presenter = createPresenter(ForwardPresenter::class); + + Assert::noError( + fn() => $presenter->run(new Application\Request('Test', Application\Request::FORWARD)), + ); +}); + + +test('forward requirement violated', function () { + $presenter = createPresenter(ForwardPresenter::class); + + Assert::exception( + fn() => $presenter->run(new Application\Request('Test', Http\Request::Get)), + Application\BadRequestException::class, + 'Forwarded request is required by ForwardPresenter', + ); +}); diff --git a/tests/UI/fixtures/ParamPresenter.php b/tests/UI/fixtures/ParamPresenter.php index 453a78c34..21e04cca4 100644 --- a/tests/UI/fixtures/ParamPresenter.php +++ b/tests/UI/fixtures/ParamPresenter.php @@ -4,7 +4,7 @@ class ParamPresenter extends Nette\Application\UI\Presenter { - /** @persistent */ + #[Nette\Application\Attributes\Persistent] public $bool = true;