From 6150e1afd97485df8c433186684b2229f0e5fa58 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 02:08:55 +0100 Subject: [PATCH 01/54] cs --- src/Application/ErrorPresenter.php | 12 +- src/Application/LinkGenerator.php | 24 +- src/Application/MicroPresenter.php | 8 +- src/Application/PresenterFactory.php | 1 + src/Application/Responses/FileResponse.php | 4 +- src/Application/Routers/Route.php | 3 +- src/Application/UI/AccessPolicy.php | 11 +- src/Application/UI/Component.php | 1 + src/Application/UI/ComponentReflection.php | 4 +- src/Application/UI/Presenter.php | 6 +- .../ApplicationDI/ApplicationExtension.php | 46 +- src/Bridges/ApplicationDI/LatteExtension.php | 5 +- .../PresenterFactoryCallback.php | 4 +- .../ApplicationDI/RoutingExtension.php | 13 +- .../ApplicationLatte/Nodes/ControlNode.php | 14 +- .../Nodes/TemplatePrintNode.php | 1 + src/Bridges/ApplicationLatte/UIExtension.php | 12 +- src/Bridges/ApplicationTracy/RoutingPanel.php | 6 +- tests/Application/Application.run.phpt | 1 - tests/Application/Helpers.splitName().phpt | 1 - tests/Application/MicroPresenter.invoke.phpt | 6 +- .../Application/MicroPresenter.response.phpt | 3 +- tests/Application/Presenter.twoDomains.phpt | 3 +- ...PresenterFactory.formatPresenterClass.phpt | 1 - tests/Application/Request.phpt | 1 - .../ApplicationExtension.basic.phpt | 10 +- .../ApplicationExtension.invalidLink.phpt | 16 +- .../Bridges.DI/ApplicationExtension.scan.phpt | 25 +- tests/Bridges.DI/LatteExtension.basic.phpt | 11 +- tests/Bridges.DI/RoutingExtension.basic.phpt | 3 +- tests/Bridges.DI/RoutingExtension.cache.phpt | 7 +- .../Template.getParentName().phpt | 9 +- .../TemplateFactory.templateVariables.84.phpt | 1 - tests/Bridges.Latte/renderSnippets.phpt | 6 +- tests/Bridges.Latte/renderSnippets2.phpt | 2 +- tests/Bridges.Latte/renderSnippets3.phpt | 4 +- tests/Bridges.Latte/renderSnippets4.phpt | 2 +- tests/Bridges.Latte/renderSnippets5.phpt | 6 +- tests/Bridges.Latte/renderSnippets6.phpt | 4 +- tests/Bridges.Latte/renderSnippets7.phpt | 34 +- tests/Bridges.Latte/{link}.2.phpt | 32 +- .../RoutingPanel.analyse.withPath.phpt | 50 +- tests/Responses/CallbackResponse.phpt | 1 - .../FileResponse.contentDisposition.phpt | 16 +- tests/Responses/FileResponse.full.phpt | 7 +- tests/Responses/FileResponse.range.phpt | 1 - tests/Responses/JsonResponse.contentType.phpt | 5 +- tests/Routers/CliRouter.invalid.phpt | 1 - tests/Routers/CliRouter.phpt | 1 - tests/Routers/LinkGenerator.phpt | 12 +- tests/Routers/Route.arrayParams.phpt | 1 - tests/Routers/Route.autooptional1.phpt | 1 - tests/Routers/Route.autooptional2.phpt | 1 - tests/Routers/Route.autooptional3.phpt | 1 - tests/Routers/Route.autooptional4.phpt | 1 - tests/Routers/Route.basic.phpt | 1 - tests/Routers/Route.camelcapsVsDash.phpt | 1 - tests/Routers/Route.closure.phpt | 1 - tests/Routers/Route.combinedUrlParam.phpt | 1 - tests/Routers/Route.errors.phpt | 1 - tests/Routers/Route.extraDefaultParam.phpt | 1 - tests/Routers/Route.filter.global.phpt | 5 +- tests/Routers/Route.filter.query.phpt | 1 - tests/Routers/Route.filter.url.object.phpt | 2 - tests/Routers/Route.filter.url.phpt | 1 - tests/Routers/Route.filterTable.query.phpt | 1 - tests/Routers/Route.filterTable.strict.phpt | 1 - tests/Routers/Route.filterTable.url.phpt | 1 - tests/Routers/Route.firstOptionalParam.phpt | 1 - tests/Routers/Route.fooParameter.default.phpt | 1 - tests/Routers/Route.fooParameter.phpt | 1 - tests/Routers/Route.fullMatchParam.phpt | 1 - tests/Routers/Route.longParameter.phpt | 1 - tests/Routers/Route.mandatoryAction.phpt | 1 - tests/Routers/Route.modules.fixity.phpt | 1 - tests/Routers/Route.modules.phpt | 1 - tests/Routers/Route.noDefaultParams.phpt | 4 +- tests/Routers/Route.noParams.phpt | 1 - .../Routers/Route.optional.autooptional1.phpt | 1 - .../Routers/Route.optional.autooptional2.phpt | 1 - .../Routers/Route.optional.autooptional3.phpt | 1 - tests/Routers/Route.optional.module.phpt | 1 - tests/Routers/Route.optional.nested.phpt | 1 - tests/Routers/Route.optional.phpt | 4 +- tests/Routers/Route.optional.precedence.phpt | 4 +- .../Route.optional.preferred.nested1.phpt | 1 - .../Route.optional.preferred.nested2.phpt | 1 - .../Route.optional.preferred.nested3.phpt | 1 - tests/Routers/Route.optional.preferred.phpt | 4 +- tests/Routers/Route.optional.regexp.phpt | 1 - tests/Routers/Route.scalarParams.phpt | 4 +- tests/Routers/Route.secured.phpt | 1 - tests/Routers/Route.slash.phpt | 1 - tests/Routers/Route.type.phpt | 1 - tests/Routers/Route.urlEncoding.phpt | 1 - tests/Routers/Route.utf8Param.phpt | 1 - tests/Routers/Route.variables.phpt | 1 - tests/Routers/Route.withAbsolutePath.phpt | 1 - .../Route.withDefaultPresenterAndAction.phpt | 1 - tests/Routers/Route.withHost.phpt | 1 - tests/Routers/Route.withHost.protocol.phpt | 1 - tests/Routers/Route.withHost.secured.phpt | 1 - .../Routers/Route.withNamedParamsInQuery.phpt | 1 - tests/Routers/Route.withParamsInQuery.phpt | 1 - tests/Routers/Route.withUserClassAlt.phpt | 1 - tests/Routers/RouteList.addRoute.phpt | 5 +- tests/Routers/RouteList.basic.phpt | 1 - tests/Routers/RouteList.modules.phpt | 1 - tests/Routers/RouteList.withModule.phpt | 1 - tests/Routers/SimpleRouter.invalid.phpt | 1 - tests/Routers/SimpleRouter.phpt | 1 - .../UI/Component.isLinkCurrent().asserts.php | 2 +- tests/UI/Component.redirect().phpt | 1 - tests/UI/Component.signals.phpt | 1 - .../ComponentReflection.parseAnnotation.phpt | 17 +- tests/UI/Control.formatTemplateClass.phpt | 1 - tests/UI/Control.isControlInvalid.phpt | 1 - tests/UI/Deprecated.link().phpt | 2 +- tests/UI/Form.phpt | 1 - .../UI/LinkGenerator.parseDestination().phpt | 5 +- tests/UI/Multiplier.phpt | 1 - tests/UI/ParameterConverter.convertType.phpt | 1 - tests/UI/Presenter.canonicalize.phpt | 3 +- tests/UI/Presenter.flashMessage.phpt | 14 +- .../Presenter.formatLayoutTemplateFiles.phpt | 1 - tests/UI/Presenter.formatTemplateClass.phpt | 1 - tests/UI/Presenter.formatTemplateFiles.phpt | 1 - .../UI/Presenter.initGlobalParameters().phpt | 1 - tests/UI/Presenter.lifecycle.phpt | 1 - tests/UI/Presenter.link().persistent.phpt | 3 +- tests/UI/Presenter.link().phpt | 480 ++++++++++++++---- tests/UI/Presenter.paramChecking.phpt | 1 - tests/UI/Presenter.redirects.phpt | 1 - tests/UI/Presenter.selfLinkInvalidParam.phpt | 1 - tests/UI/Presenter.storeRequest().phpt | 5 +- tests/UI/Requires.forward.link().phpt | 2 +- tests/UI/Requires.locations.phpt | 1 - tests/UI/fixtures/ParamPresenter.php | 2 +- tests/UI/functions.php | 1 - tests/bootstrap.php | 6 +- tests/types/application-types.php | 4 +- 141 files changed, 690 insertions(+), 414 deletions(-) diff --git a/src/Application/ErrorPresenter.php b/src/Application/ErrorPresenter.php index 01cce11e5..271b74bd5 100644 --- a/src/Application/ErrorPresenter.php +++ b/src/Application/ErrorPresenter.php @@ -32,10 +32,12 @@ public function run(Application\Request $request): Application\Response $this->logger?->log($e, ILogger::EXCEPTION); } - return new Application\Responses\CallbackResponse(function (Http\IRequest $httpRequest, Http\IResponse $httpResponse) use ($code): void { - if (preg_match('#^text/html(?:;|$)#', (string) $httpResponse->getHeader('Content-Type'))) { - require __DIR__ . '/templates/error.phtml'; - } - }); + return new Application\Responses\CallbackResponse( + function (Http\IRequest $httpRequest, Http\IResponse $httpResponse) use ($code): void { + if (preg_match('#^text/html(?:;|$)#', (string) $httpResponse->getHeader('Content-Type'))) { + require __DIR__ . '/templates/error.phtml'; + } + }, + ); } } diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index 2fbc0d1f6..fccf341c7 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -154,7 +154,12 @@ public function createRequest( throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()"); } - $this->validateLinkTarget($refPresenter, $method, "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class), $mode); + $this->validateLinkTarget( + $refPresenter, + $method, + "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class), + $mode, + ); // convert indexed parameters to named UI\ParameterConverter::toParameters($method, $args, [], $missing); @@ -180,7 +185,9 @@ public function createRequest( $action = UI\Presenter::DefaultAction; } - $current = $refPresenter && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) && $presenterClass === $refPresenter::class; + $current = $refPresenter + && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) + && $presenterClass === $refPresenter::class; $reflection = new UI\ComponentReflection($presenterClass); $this->validateLinkTarget($refPresenter, $reflection, "presenter '$presenter'", $mode); @@ -195,7 +202,12 @@ public function createRequest( if ($method = $reflection->getActionRenderMethod($action)) { $this->validateLinkTarget($refPresenter, $method, "action '$presenter:$action'", $mode); - UI\ParameterConverter::toParameters($method, $args, $path === 'this' ? ($refPresenter?->getParameters() ?? []) : [], $missing); + UI\ParameterConverter::toParameters( + $method, + $args, + $path === 'this' ? ($refPresenter?->getParameters() ?? []) : [], + $missing, + ); } elseif (array_key_exists(0, $args)) { throw new UI\InvalidLinkException("Unable to pass parameters to action '$presenter:$action', missing corresponding method $presenterClass::{$presenterClass::formatRenderMethod($action)}()."); @@ -258,7 +270,11 @@ public function createRequest( */ public static function parseDestination(string $destination): array { - if (!preg_match('~^ (?//)?+ (?[^!?#]++) (?!)?+ (?\?[^#]*)?+ (?\#.*)?+ $~x', $destination, $matches)) { + if (!preg_match( + '~^ (?//)?+ (?[^!?#]++) (?!)?+ (?\?[^#]*)?+ (?\#.*)?+ $~x', + $destination, + $matches, + )) { throw new UI\InvalidLinkException("Invalid destination '$destination'."); } diff --git a/src/Application/MicroPresenter.php b/src/Application/MicroPresenter.php index bc2aff8cf..bd0a019c9 100644 --- a/src/Application/MicroPresenter.php +++ b/src/Application/MicroPresenter.php @@ -94,11 +94,9 @@ public function run(Application\Request $request): Application\Response $response->setFile((string) $templateSource); } - if ($response instanceof Application\UI\Template) { - return new Responses\TextResponse($response); - } else { - return $response ?: new Responses\VoidResponse; - } + return $response instanceof Application\UI\Template + ? new Responses\TextResponse($response) + : ($response ?: new Responses\VoidResponse); } diff --git a/src/Application/PresenterFactory.php b/src/Application/PresenterFactory.php index db61f261f..1686211fb 100644 --- a/src/Application/PresenterFactory.php +++ b/src/Application/PresenterFactory.php @@ -93,6 +93,7 @@ public function setMapping(array $mapping): static } $this->mapping[$module] = [$m[1], $m[2] ?: '*Module\\', $m[3]]; + } elseif (is_array($mask) && count($mask) === 3) { $this->mapping[$module] = [$mask[0] ? $mask[0] . '\\' : '', $mask[1] . '\\', $mask[2]]; } else { diff --git a/src/Application/Responses/FileResponse.php b/src/Application/Responses/FileResponse.php index beda43838..95bd1b22f 100644 --- a/src/Application/Responses/FileResponse.php +++ b/src/Application/Responses/FileResponse.php @@ -70,8 +70,8 @@ public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $htt $httpResponse->setHeader( 'Content-Disposition', ($this->forceDownload ? 'attachment' : 'inline') - . '; filename="' . $this->name . '"' - . '; filename*=utf-8\'\'' . rawurlencode($this->name), + . '; filename="' . $this->name . '"' + . '; filename*=utf-8\'\'' . rawurlencode($this->name), ); $filesize = $length = filesize($this->file); diff --git a/src/Application/Routers/Route.php b/src/Application/Routers/Route.php index eb033620c..867113e26 100644 --- a/src/Application/Routers/Route.php +++ b/src/Application/Routers/Route.php @@ -42,7 +42,8 @@ class Route extends Nette\Routing\Route implements Nette\Routing\Router /** * @param string $mask e.g. '//' - * @param array|string|\Closure(Nette\Http\IRequest, Nette\Http\IResponse): Nette\Application\Response $metadata default values or metadata or callback for NetteModule\MicroPresenter + * @param array|string|\Closure(Nette\Http\IRequest, Nette\Http\IResponse): Nette\Application\Response $metadata + * default values or metadata or callback for NetteModule\MicroPresenter */ public function __construct(string $mask, array|string|\Closure $metadata = []) { diff --git a/src/Application/UI/AccessPolicy.php b/src/Application/UI/AccessPolicy.php index 0b372aa5e..61917f78e 100644 --- a/src/Application/UI/AccessPolicy.php +++ b/src/Application/UI/AccessPolicy.php @@ -21,8 +21,8 @@ final class AccessPolicy private Presenter $presenter; - /** @param \ReflectionClass|\ReflectionMethod $element */ public function __construct( + /** @var \ReflectionClass|\ReflectionMethod */ private readonly \ReflectionClass|\ReflectionMethod $element, ) { } @@ -30,8 +30,8 @@ public function __construct( public function checkAccess(Component $component): void { - $this->presenter ??= $component->getPresenter(throw: false) ?? - throw new Nette\InvalidStateException('Presenter is required for checking requirements of ' . Reflection::toString($this->element)); + $this->presenter ??= $component->getPresenter(throw: false) + ?? throw new Nette\InvalidStateException('Presenter is required for checking requirements of ' . Reflection::toString($this->element)); $attrs = $this->getAttributes(); $attrs = self::applyInternalRules($attrs, $component); @@ -97,7 +97,10 @@ private function checkAttribute(Attributes\Requires $attribute): void } if ($attribute->ajax && !$this->presenter->getHttpRequest()->isAjax()) { - $this->presenter->error('AJAX request is required by ' . Reflection::toString($this->element), Nette\Http\IResponse::S403_Forbidden); + $this->presenter->error( + 'AJAX request is required by ' . Reflection::toString($this->element), + Nette\Http\IResponse::S403_Forbidden, + ); } } diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index 4508ae19c..e67ba2ffc 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -165,6 +165,7 @@ public function loadState(array $params): void } $this->$name = $params[$name]; + } else { $params[$name] = $this->$name ?? null; } diff --git a/src/Application/UI/ComponentReflection.php b/src/Application/UI/ComponentReflection.php index 3906a9a05..4e7012233 100644 --- a/src/Application/UI/ComponentReflection.php +++ b/src/Application/UI/ComponentReflection.php @@ -107,7 +107,7 @@ public function getPersistentComponents(): array $names = $attrs ? $attrs[0]->getArguments() : (array) self::parseAnnotation($this, 'persistent'); - $names = array_merge($names, $class::getPersistentComponents()); + $names = [...$names, ...$class::getPersistentComponents()]; $components = array_fill_keys($names, ['since' => $class]); if ($this->isSubclassOf(Presenter::class)) { @@ -199,7 +199,7 @@ public static function parseAnnotation(\Reflector $ref, string $name): ?array 'persistent' => '#[Nette\Application\Attributes\Persistent]', 'deprecated' => '#[Nette\Application\Attributes\Deprecated]', 'crossOrigin' => '#[Nette\Application\Attributes\Requires(sameOrigin: false)]', - default => 'alternative' + default => 'alternative', }; trigger_error("Annotation @$name is deprecated, use $alt (used in " . Reflection::toString($ref) . ')', E_USER_DEPRECATED); return $res; diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index e0354e948..f0160d0e7 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -338,8 +338,8 @@ public function detectedCsrf(): void /** @deprecated use #[Requires(methods: ...)] to specify allowed methods */ protected function checkHttpMethod(): void { - if ($this->allowedMethods && - !in_array($method = $this->httpRequest->getMethod(), $this->allowedMethods, true) + if ($this->allowedMethods + && !in_array($method = $this->httpRequest->getMethod(), $this->allowedMethods, true) ) { $this->httpResponse->setHeader('Allow', implode(',', $this->allowedMethods)); $this->error("Method $method is not allowed", Nette\Http\IResponse::S405_MethodNotAllowed); @@ -931,7 +931,7 @@ public function restoreRequest(string $key): void $params[self::FlashKey] = $this->getFlashKey(); $request->setParameters($params); if ($request->isMethod('POST')) { - $request->setFlag(Application\Request::RESTORED, true); + $request->setFlag(Application\Request::RESTORED); $this->sendResponse(new Responses\ForwardResponse($request)); } else { $this->redirectUrl($this->linkGenerator->requestToUrl($request)); diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php index 424d67368..c95f478ba 100644 --- a/src/Bridges/ApplicationDI/ApplicationExtension.php +++ b/src/Bridges/ApplicationDI/ApplicationExtension.php @@ -92,10 +92,16 @@ public function loadConfiguration(): void : UI\Presenter::InvalidLinkWarning; $application = $builder->addDefinition($this->prefix('application')) - ->setFactory(Nette\Application\Application::class); + ->setCreator(Nette\Application\Application::class); if ($config->catchExceptions || !$this->debugMode) { - $application->addSetup('$error4xxPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['4xx'] : $config->errorPresenter]); - $application->addSetup('$errorPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['5xx'] : $config->errorPresenter]); + $application->addSetup( + '$error4xxPresenter', + [is_array($config->errorPresenter) ? $config->errorPresenter['4xx'] : $config->errorPresenter], + ); + $application->addSetup( + '$errorPresenter', + [is_array($config->errorPresenter) ? $config->errorPresenter['5xx'] : $config->errorPresenter], + ); } $this->compiler->addExportedType(Nette\Application\Application::class); @@ -108,7 +114,7 @@ public function loadConfiguration(): void $presenterFactory = $builder->addDefinition($this->prefix('presenterFactory')) ->setType(Nette\Application\IPresenterFactory::class) - ->setFactory(Nette\Application\PresenterFactory::class, [new Definitions\Statement( + ->setCreator(Nette\Application\PresenterFactory::class, [new Definitions\Statement( Nette\Bridges\ApplicationDI\PresenterFactoryCallback::class, [1 => $touch ?? null], )]); @@ -124,7 +130,7 @@ public function loadConfiguration(): void } $builder->addDefinition($this->prefix('linkGenerator')) - ->setFactory(Nette\Application\LinkGenerator::class, [ + ->setCreator(Nette\Application\LinkGenerator::class, [ 1 => new Definitions\Statement([new Definitions\Statement('@Nette\Http\IRequest::getUrl'), 'withoutUserInfo']), ]); @@ -208,7 +214,7 @@ private function findPresenters(): array $classFile = dirname((string) $rc->getFileName()) . '/autoload_classmap.php'; if (is_file($classFile)) { $this->getContainerBuilder()->addDependency($classFile); - $classes = array_merge($classes, array_keys((fn($path) => require $path)($classFile))); + $classes = [...$classes, ...array_keys((fn($path) => require $path)($classFile))]; } } @@ -286,22 +292,40 @@ private function checkPresenter(string $class): void if (($rp->getAttributes($attr = Attributes\Parameter::class) || $rp->getAttributes($attr = Attributes\Persistent::class)) && (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly()) ) { - throw new Nette\InvalidStateException(sprintf('Property %s: attribute %s can be used only with public non-static property.', Reflection::toString($rp), $attr)); + throw new Nette\InvalidStateException(sprintf( + 'Property %s: attribute %s can be used only with public non-static property.', + Reflection::toString($rp), + $attr, + )); } } $re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.'; foreach ($rc->getMethods() as $rm) { if (preg_match("#^(?!handleInvalidLink)($re)#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) { - throw new Nette\InvalidStateException(sprintf('Method %s: this method must be public non-static.', Reflection::toString($rm))); + 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))); + throw new Nette\InvalidStateException(sprintf( + 'Method %s: this method must be non-private non-static.', + Reflection::toString($rm), + )); } elseif ($rm->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF) && !preg_match("#^$re|createComponent.#", $rm->getName()) ) { - throw new Nette\InvalidStateException(sprintf('Method %s: attribute %s can be used only with action, render, handle or createComponent methods.', Reflection::toString($rm), Attributes\Requires::class)); + throw new Nette\InvalidStateException(sprintf( + 'Method %s: attribute %s can be used only with action, render, handle or createComponent methods.', + Reflection::toString($rm), + Attributes\Requires::class, + )); } elseif ($rm->getAttributes(Attributes\Deprecated::class) && !preg_match("#^$re#", $rm->getName())) { - throw new Nette\InvalidStateException(sprintf('Method %s: attribute %s can be used only with action, render or handle methods.', Reflection::toString($rm), Attributes\Deprecated::class)); + throw new Nette\InvalidStateException(sprintf( + 'Method %s: attribute %s can be used only with action, render or handle methods.', + Reflection::toString($rm), + Attributes\Deprecated::class, + )); } } } diff --git a/src/Bridges/ApplicationDI/LatteExtension.php b/src/Bridges/ApplicationDI/LatteExtension.php index 9d8abd149..62c04ab7a 100644 --- a/src/Bridges/ApplicationDI/LatteExtension.php +++ b/src/Bridges/ApplicationDI/LatteExtension.php @@ -21,7 +21,6 @@ * * @property object{ * debugger: bool|'all'|null, - * macros: array, * extensions: array, * templateClass: string|null, * strictTypes: bool, @@ -69,7 +68,7 @@ public function loadConfiguration(): void $builder->addFactoryDefinition($this->prefix('latteFactory')) ->setImplement(ApplicationLatte\LatteFactory::class) ->getResultDefinition() - ->setFactory(Latte\Engine::class) + ->setCreator(Latte\Engine::class) ->addSetup('setCacheDirectory', [$this->tempDir]) ->addSetup('setAutoRefresh', [$this->debugMode]) ->addSetup('setFeature', [Latte\Feature::StrictTypes, $config->strictTypes]) @@ -95,7 +94,7 @@ public function loadConfiguration(): void } $builder->addDefinition($this->prefix('templateFactory')) - ->setFactory(ApplicationLatte\TemplateFactory::class) + ->setCreator(ApplicationLatte\TemplateFactory::class) ->setArguments(['templateClass' => $config->templateClass]); if ($this->name === 'latte') { diff --git a/src/Bridges/ApplicationDI/PresenterFactoryCallback.php b/src/Bridges/ApplicationDI/PresenterFactoryCallback.php index 3a50f5ef9..fa5df5975 100644 --- a/src/Bridges/ApplicationDI/PresenterFactoryCallback.php +++ b/src/Bridges/ApplicationDI/PresenterFactoryCallback.php @@ -43,7 +43,9 @@ public function __invoke(string $class): Nette\Application\IPresenter 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.'; + 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; } diff --git a/src/Bridges/ApplicationDI/RoutingExtension.php b/src/Bridges/ApplicationDI/RoutingExtension.php index 7ceae3a01..360f2cfe4 100644 --- a/src/Bridges/ApplicationDI/RoutingExtension.php +++ b/src/Bridges/ApplicationDI/RoutingExtension.php @@ -49,7 +49,7 @@ public function loadConfiguration(): void $builder = $this->getContainerBuilder(); $router = $builder->addDefinition($this->prefix('router')) - ->setFactory(Nette\Application\Routers\RouteList::class); + ->setCreator(Nette\Application\Routers\RouteList::class); foreach ($this->config->routes as $mask => $action) { $router->addSetup('$service->addRoute(?, ?)', [$mask, $action]); @@ -66,10 +66,10 @@ public function beforeCompile(): void $builder = $this->getContainerBuilder(); if ( - $this->debugMode && - ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class)) && - ($name = $builder->getByType(Nette\Application\Application::class)) && - ($application = $builder->getDefinition($name)) instanceof Definitions\ServiceDefinition + $this->debugMode + && ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class)) + && ($name = $builder->getByType(Nette\Application\Application::class)) + && ($application = $builder->getDefinition($name)) instanceof Definitions\ServiceDefinition ) { $application->addSetup('@Tracy\Bar::addPanel', [ new Definitions\Statement(Nette\Bridges\ApplicationTracy\RoutingPanel::class), @@ -79,7 +79,7 @@ public function beforeCompile(): void if (!$builder->getByType(Nette\Routing\Router::class)) { $builder->addDefinition($this->prefix('router')) ->setType(Nette\Routing\Router::class) - ->setFactory(Nette\Routing\SimpleRouter::class); + ->setCreator(Nette\Routing\SimpleRouter::class); $builder->addAlias('router', $this->prefix('router')); } } @@ -98,6 +98,7 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void } $s = serialize($router); + } catch (\Throwable $e) { throw new Nette\DI\ServiceCreationException('Unable to cache router due to error: ' . $e->getMessage(), 0, $e); } diff --git a/src/Bridges/ApplicationLatte/Nodes/ControlNode.php b/src/Bridges/ApplicationLatte/Nodes/ControlNode.php index a93a413b2..5bb6f30a5 100644 --- a/src/Bridges/ApplicationLatte/Nodes/ControlNode.php +++ b/src/Bridges/ApplicationLatte/Nodes/ControlNode.php @@ -77,7 +77,8 @@ public function print(PrintContext $context): string $method = match (true) { !$this->method => 'render', - $this->method instanceof StringNode && Strings::match($this->method->value, '#^\w*$#D') => 'render' . ucfirst($this->method->value), + $this->method instanceof StringNode && Strings::match($this->method->value, '#^\w*$#D') => 'render' + . ucfirst($this->method->value), default => "{'render' . " . $this->method->print($context) . '}', }; @@ -88,8 +89,8 @@ public function print(PrintContext $context): string $this->name, ); - if ($this->escape) { - return $context->format( + return $this->escape + ? $context->format( <<<'XX' %raw if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); @@ -105,10 +106,8 @@ public function print(PrintContext $context): string $this->position, Latte\ContentType::Html, new ModifierNode([], $this->escape), - ); - - } else { - return $context->format( + ) + : $context->format( <<<'XX' %raw if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); @@ -121,7 +120,6 @@ public function print(PrintContext $context): string $this->args, $this->position, ); - } } diff --git a/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php b/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php index 885ecd242..4d585e4ea 100644 --- a/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php +++ b/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php @@ -12,6 +12,7 @@ use Nette\Application\UI; use Nette\Bridges\ApplicationLatte\Template; + /** * {templatePrint [ClassName]} * Generates template class extending Nette\Bridges\ApplicationLatte\Template. diff --git a/src/Bridges/ApplicationLatte/UIExtension.php b/src/Bridges/ApplicationLatte/UIExtension.php index b934e59bd..da10e9412 100644 --- a/src/Bridges/ApplicationLatte/UIExtension.php +++ b/src/Bridges/ApplicationLatte/UIExtension.php @@ -34,11 +34,13 @@ public function getFilters(): array 'modifyDate' => fn($time, $delta, $unit = null) => $time ? Nette\Utils\DateTime::from($time)->modify($delta . $unit) : null, - ] + ($presenter ? [ - 'absoluteUrl' => fn(\Stringable|string|null $link): ?string => $link === null + ] + ($presenter + ? [ + 'absoluteUrl' => fn(\Stringable|string|null $link): ?string => $link === null ? null : $presenter->getHttpRequest()->getUrl()->resolve((string) $link)->getAbsoluteUrl(), - ] : []); + ] + : []); } @@ -130,7 +132,9 @@ public static function createExtendsNode(Tag $tag): ExtendsNode $auto = $tag->parser->stream->is('auto'); $node = ExtendsNode::create($tag); if ($auto) { - $node->extends = new AuxiliaryNode(fn() => '($this->getReferringTemplate() ? null : $this->global->uiPresenter->findLayoutTemplateFile())'); + $node->extends = new AuxiliaryNode( + fn() => '($this->getReferringTemplate() ? null : $this->global->uiPresenter->findLayoutTemplateFile())', + ); } return $node; } diff --git a/src/Bridges/ApplicationTracy/RoutingPanel.php b/src/Bridges/ApplicationTracy/RoutingPanel.php index 6ada0036d..aee22e113 100644 --- a/src/Bridges/ApplicationTracy/RoutingPanel.php +++ b/src/Bridges/ApplicationTracy/RoutingPanel.php @@ -88,7 +88,7 @@ private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRe continue; } - $matched = $flags[$i] & $router::ONE_WAY ? 'oneway' : 'no'; + $matched = $flags[$i] ? 'oneway' : 'no'; $params = $e = null; try { if ( @@ -109,7 +109,9 @@ private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRe $res['routes'][] = (object) [ 'matched' => $matched, 'class' => $innerRouter::class, - 'defaults' => $innerRouter instanceof Routing\Route || $innerRouter instanceof Routing\SimpleRouter ? $innerRouter->getDefaults() : [], + 'defaults' => $innerRouter instanceof Routing\Route || $innerRouter instanceof Routing\SimpleRouter + ? $innerRouter->getDefaults() + : [], 'mask' => $innerRouter instanceof Routing\Route ? $innerRouter->getMask() : null, 'params' => $params, 'error' => $e, diff --git a/tests/Application/Application.run.phpt b/tests/Application/Application.run.phpt index d7381ef68..400cacd7a 100644 --- a/tests/Application/Application.run.phpt +++ b/tests/Application/Application.run.phpt @@ -5,7 +5,6 @@ use Nette\Application\Responses\{ForwardResponse, TextResponse}; use Nette\Routing\Router; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Application/Helpers.splitName().phpt b/tests/Application/Helpers.splitName().phpt index 90f29b2d9..1ca77bd5a 100644 --- a/tests/Application/Helpers.splitName().phpt +++ b/tests/Application/Helpers.splitName().phpt @@ -7,7 +7,6 @@ use Nette\Application\Helpers; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Application/MicroPresenter.invoke.phpt b/tests/Application/MicroPresenter.invoke.phpt index db8f1aa2a..a989d66c7 100644 --- a/tests/Application/MicroPresenter.invoke.phpt +++ b/tests/Application/MicroPresenter.invoke.phpt @@ -7,7 +7,6 @@ use Nette\Application\Request; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -30,7 +29,10 @@ test('callback invocation with parameters', function () { test('dependency injection in callback', function () { $container = Mockery::mock(Nette\DI\Container::class) - ->shouldReceive('getByType')->with('stdClass', false)->once()->andReturn(new stdClass) + ->shouldReceive('getByType') + ->with('stdClass', false) + ->once() + ->andReturn(new stdClass) ->mock(); $presenter = new NetteModule\MicroPresenter($container); diff --git a/tests/Application/MicroPresenter.response.phpt b/tests/Application/MicroPresenter.response.phpt index 966002840..600b337a5 100644 --- a/tests/Application/MicroPresenter.response.phpt +++ b/tests/Application/MicroPresenter.response.phpt @@ -8,7 +8,6 @@ use Nette\Application\Request; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -30,7 +29,7 @@ function createContainer() $latteFactory = Mockery::mock(Nette\Bridges\ApplicationLatte\LatteFactory::class); $latteFactory->shouldReceive('create')->andReturn(new Latte\Engine); $container = Mockery::mock(Nette\DI\Container::class); - $container->shouldReceive('getByType')->with('Nette\Bridges\ApplicationLatte\LatteFactory')->andReturn($latteFactory); + $container->shouldReceive('getByType')->with(Nette\Bridges\ApplicationLatte\LatteFactory::class)->andReturn($latteFactory); return $container; } diff --git a/tests/Application/Presenter.twoDomains.phpt b/tests/Application/Presenter.twoDomains.phpt index 47cbbaef0..bbf089ad2 100644 --- a/tests/Application/Presenter.twoDomains.phpt +++ b/tests/Application/Presenter.twoDomains.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -31,7 +30,7 @@ function testLink($domain) new Application\Routers\SimpleRouter, ); - $request = new Application\Request('Test', Http\Request::Get, []); + $request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); Assert::same('http://' . $domain . '/index.php?action=default&presenter=Test', $presenter->link('//this')); diff --git a/tests/Application/PresenterFactory.formatPresenterClass.phpt b/tests/Application/PresenterFactory.formatPresenterClass.phpt index 080709ef3..67db5c807 100644 --- a/tests/Application/PresenterFactory.formatPresenterClass.phpt +++ b/tests/Application/PresenterFactory.formatPresenterClass.phpt @@ -7,7 +7,6 @@ use Nette\Application\PresenterFactory; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Application/Request.phpt b/tests/Application/Request.phpt index 42d61d576..9f8de1d38 100644 --- a/tests/Application/Request.phpt +++ b/tests/Application/Request.phpt @@ -7,7 +7,6 @@ use Nette\Application\Request; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Bridges.DI/ApplicationExtension.basic.phpt b/tests/Bridges.DI/ApplicationExtension.basic.phpt index b5c89830d..39b40903c 100644 --- a/tests/Bridges.DI/ApplicationExtension.basic.phpt +++ b/tests/Bridges.DI/ApplicationExtension.basic.phpt @@ -4,18 +4,18 @@ use Nette\Bridges\ApplicationDI\ApplicationExtension; use Nette\DI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; test('', function () { $compiler = new DI\Compiler; - $compiler->addExtension('application', new ApplicationExtension(false)); + $compiler->addExtension('application', new ApplicationExtension); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new DI\Definitions\Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest') + ->setCreator(Nette\Http\Request::class, [new DI\Definitions\Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); $code = $compiler->setClassName('Container1')->compile(); eval($code); diff --git a/tests/Bridges.DI/ApplicationExtension.invalidLink.phpt b/tests/Bridges.DI/ApplicationExtension.invalidLink.phpt index d8117e63b..4389cab21 100644 --- a/tests/Bridges.DI/ApplicationExtension.invalidLink.phpt +++ b/tests/Bridges.DI/ApplicationExtension.invalidLink.phpt @@ -5,7 +5,6 @@ use Nette\Bridges\ApplicationDI\ApplicationExtension; use Nette\DI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/files/MyPresenter.php'; @@ -15,9 +14,10 @@ function createCompiler(string $config): DI\Compiler $compiler = new DI\Compiler; $compiler->loadConfig(Tester\FileMock::create($config, 'neon')); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new DI\Definitions\Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest') + ->setCreator(Nette\Http\Request::class, [new DI\Definitions\Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); return $compiler; } @@ -30,7 +30,7 @@ test('silentLinks enabled with debugger', function () { services: presenter: Presenter1 '); - $compiler->addExtension('application', new ApplicationExtension(true)); + $compiler->addExtension('application', new ApplicationExtension(debugMode: true)); $code = $compiler->setClassName('Container4')->compile(); eval($code); @@ -50,7 +50,7 @@ test('silentLinks disabled with debugger', function () { services: presenter: Presenter1 '); - $compiler->addExtension('application', new ApplicationExtension(true)); + $compiler->addExtension('application', new ApplicationExtension(debugMode: true)); $code = $compiler->setClassName('Container5')->compile(); eval($code); @@ -70,7 +70,7 @@ test('silentLinks enabled without debugger', function () { services: presenter: Presenter1 '); - $compiler->addExtension('application', new ApplicationExtension(false)); + $compiler->addExtension('application', new ApplicationExtension); $code = $compiler->setClassName('Container6')->compile(); eval($code); @@ -90,7 +90,7 @@ test('silentLinks disabled without debugger', function () { services: presenter: Presenter1 '); - $compiler->addExtension('application', new ApplicationExtension(false)); + $compiler->addExtension('application', new ApplicationExtension); $code = $compiler->setClassName('Container7')->compile(); eval($code); diff --git a/tests/Bridges.DI/ApplicationExtension.scan.phpt b/tests/Bridges.DI/ApplicationExtension.scan.phpt index af97615b5..cc1b473a9 100644 --- a/tests/Bridges.DI/ApplicationExtension.scan.phpt +++ b/tests/Bridges.DI/ApplicationExtension.scan.phpt @@ -5,7 +5,6 @@ use Nette\DI; use Nette\DI\Definitions\Statement; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/files/MyPresenter.php'; @@ -15,9 +14,9 @@ test('default presenter registration', function () { $compiler->addExtension('application', new ApplicationExtension); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest')->setCreator(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); $code = $compiler->setClassName('Container1')->compile(); eval($code); @@ -33,9 +32,9 @@ test('scanDirs with filter', function () { $compiler->addExtension('application', new ApplicationExtension); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest')->setCreator(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); $code = $compiler->addConfig([ 'application' => [ 'scanDirs' => [__DIR__ . '/files'], @@ -56,9 +55,9 @@ test('combined scanDirs and config setup', function () { $compiler->addExtension('application', new ApplicationExtension(false, [__DIR__ . '/files'])); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest')->setCreator(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); $loader = new DI\Config\Loader; $config = $loader->load(Tester\FileMock::create(' application: @@ -92,9 +91,9 @@ test('robotLoader discovery', function () { $compiler->addExtension('application', new ApplicationExtension(false, null, null, $robot)); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest')->setCreator(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); $code = $compiler->setClassName('Container4')->compile(); eval($code); diff --git a/tests/Bridges.DI/LatteExtension.basic.phpt b/tests/Bridges.DI/LatteExtension.basic.phpt index 7acde0faf..944ee5bff 100644 --- a/tests/Bridges.DI/LatteExtension.basic.phpt +++ b/tests/Bridges.DI/LatteExtension.basic.phpt @@ -4,6 +4,8 @@ * Test: LatteExtension v3 */ +// dresscode:disable hygiene.defaultArgument -- the named arguments say what the call does + use Nette\DI; use Tester\Assert; @@ -12,12 +14,9 @@ require __DIR__ . '/../bootstrap.php'; class MyExtension extends Latte\Extension { - public $arg; - - - public function __construct($arg = null) - { - $this->arg = $arg; + public function __construct( + public $arg = null, + ) { } } diff --git a/tests/Bridges.DI/RoutingExtension.basic.phpt b/tests/Bridges.DI/RoutingExtension.basic.phpt index 5b45556f7..c20a3816b 100644 --- a/tests/Bridges.DI/RoutingExtension.basic.phpt +++ b/tests/Bridges.DI/RoutingExtension.basic.phpt @@ -8,7 +8,6 @@ use Nette\Bridges\ApplicationDI\RoutingExtension; use Nette\DI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -22,7 +21,7 @@ test('', function () { ', 'neon')); $compiler = new DI\Compiler; - $compiler->addExtension('routing', new RoutingExtension(false)); + $compiler->addExtension('routing', new RoutingExtension); $code = $compiler->addConfig($config)->setClassName('Container1')->compile(); eval($code); diff --git a/tests/Bridges.DI/RoutingExtension.cache.phpt b/tests/Bridges.DI/RoutingExtension.cache.phpt index 99233b416..ef97ce23d 100644 --- a/tests/Bridges.DI/RoutingExtension.cache.phpt +++ b/tests/Bridges.DI/RoutingExtension.cache.phpt @@ -9,7 +9,6 @@ use Nette\Bridges\ApplicationDI\RoutingExtension; use Nette\DI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -43,7 +42,7 @@ test('router without cache', function () { ', 'neon')); $compiler = new DI\Compiler; - $compiler->addExtension('routing', new RoutingExtension(false)); + $compiler->addExtension('routing', new RoutingExtension); $code = $compiler->addConfig($config)->setClassName('Container1')->compile(); eval($code); @@ -64,7 +63,7 @@ test('router with cache', function () { ', 'neon')); $compiler = new DI\Compiler; - $compiler->addExtension('routing', new RoutingExtension(false)); + $compiler->addExtension('routing', new RoutingExtension); $code = $compiler->addConfig($config)->setClassName('Container2')->compile(); eval($code); @@ -91,6 +90,6 @@ testException('non-cacheable router factory', function () { ', 'neon')); $compiler = new DI\Compiler; - $compiler->addExtension('routing', new RoutingExtension(false)); + $compiler->addExtension('routing', new RoutingExtension); $code = $compiler->addConfig($config)->setClassName('Container3')->compile(); }, Nette\DI\ServiceCreationException::class, 'Unable to cache router due to error: %a%'); diff --git a/tests/Bridges.Latte/Template.getParentName().phpt b/tests/Bridges.Latte/Template.getParentName().phpt index 4579424a5..80bc51163 100644 --- a/tests/Bridges.Latte/Template.getParentName().phpt +++ b/tests/Bridges.Latte/Template.getParentName().phpt @@ -7,9 +7,12 @@ require __DIR__ . '/../bootstrap.php'; Tester\Environment::bypassFinals(); $presenter = Mockery::mock(Nette\Application\UI\Presenter::class) - ->shouldReceive('getPresenter')->andReturnSelf() - ->shouldReceive('getHttpResponse')->andReturn(Mockery::mock(Nette\Http\IResponse::class)->shouldIgnoreMissing()) - ->shouldReceive('findLayoutTemplateFile')->andReturn('layout.latte') + ->shouldReceive('getPresenter') + ->andReturnSelf() + ->shouldReceive('getHttpResponse') + ->andReturn(Mockery::mock(Nette\Http\IResponse::class)->shouldIgnoreMissing()) + ->shouldReceive('findLayoutTemplateFile') + ->andReturn('layout.latte') ->mock(); $latte = new Latte\Engine; diff --git a/tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt b/tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt index ca550da02..c8c1ea492 100644 --- a/tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt +++ b/tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt @@ -5,7 +5,6 @@ * @phpVersion 8.4 */ -use Latte\Attributes\{TemplateFilter, TemplateFunction}; use Nette\Application\Attributes\TemplateVariable; use Nette\Bridges\ApplicationLatte\{LatteFactory, TemplateFactory}; use Nette\Http; diff --git a/tests/Bridges.Latte/renderSnippets.phpt b/tests/Bridges.Latte/renderSnippets.phpt index 282b5a74d..773fc6a2c 100644 --- a/tests/Bridges.Latte/renderSnippets.phpt +++ b/tests/Bridges.Latte/renderSnippets.phpt @@ -43,7 +43,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl(); $presenter['multi-1']->redrawControl(); @@ -67,7 +67,7 @@ Assert::same([ $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('hello'); $presenter->redrawControl('array'); @@ -83,7 +83,7 @@ Assert::same([ ], (array) $presenter->payload); $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); ob_start(); $presenter->render(); $content = ob_get_clean(); diff --git a/tests/Bridges.Latte/renderSnippets2.phpt b/tests/Bridges.Latte/renderSnippets2.phpt index 657882a1b..cadff31b3 100644 --- a/tests/Bridges.Latte/renderSnippets2.phpt +++ b/tests/Bridges.Latte/renderSnippets2.phpt @@ -58,7 +58,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter['multi-1']->redrawControl(); $presenter->render(); diff --git a/tests/Bridges.Latte/renderSnippets3.phpt b/tests/Bridges.Latte/renderSnippets3.phpt index 68c600822..8d42c1ec0 100644 --- a/tests/Bridges.Latte/renderSnippets3.phpt +++ b/tests/Bridges.Latte/renderSnippets3.phpt @@ -40,7 +40,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); $presenter['test']->redrawControl('foo'); @@ -53,7 +53,7 @@ Assert::same([ $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter['test']->redrawControl('foo'); $presenter->render(); diff --git a/tests/Bridges.Latte/renderSnippets4.phpt b/tests/Bridges.Latte/renderSnippets4.phpt index ddb8ea1fa..d264351af 100644 --- a/tests/Bridges.Latte/renderSnippets4.phpt +++ b/tests/Bridges.Latte/renderSnippets4.phpt @@ -32,7 +32,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); $presenter->render(); diff --git a/tests/Bridges.Latte/renderSnippets5.phpt b/tests/Bridges.Latte/renderSnippets5.phpt index 000acb7c3..0d0137496 100644 --- a/tests/Bridges.Latte/renderSnippets5.phpt +++ b/tests/Bridges.Latte/renderSnippets5.phpt @@ -23,7 +23,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); $presenter->render('
Hello
'); @@ -35,7 +35,7 @@ Assert::same([ $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); Assert::exception( @@ -47,7 +47,7 @@ Assert::exception( $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); Assert::exception( diff --git a/tests/Bridges.Latte/renderSnippets6.phpt b/tests/Bridges.Latte/renderSnippets6.phpt index 3b1693331..97f283c50 100644 --- a/tests/Bridges.Latte/renderSnippets6.phpt +++ b/tests/Bridges.Latte/renderSnippets6.phpt @@ -24,7 +24,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); ob_start(); $presenter->render('
hello
'); $content = ob_get_clean(); @@ -32,7 +32,7 @@ Assert::same('
hello
', $content); $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); Assert::exception( fn() => $presenter->render('
hello
'), Latte\CompileException::class, diff --git a/tests/Bridges.Latte/renderSnippets7.phpt b/tests/Bridges.Latte/renderSnippets7.phpt index e3c254e0c..0a0695578 100644 --- a/tests/Bridges.Latte/renderSnippets7.phpt +++ b/tests/Bridges.Latte/renderSnippets7.phpt @@ -10,21 +10,21 @@ require __DIR__ . '/ControlMock.php'; $dataSets = [ - //simple snippet + // simple snippet [ ['main' => '{snippet foo}hello{/snippet}'], ['foo' => 'hello'], '
hello
', ['foo'], ], - //snippetArea with nested snippet + // snippetArea with nested snippet [ ['main' => '{snippetArea fooWrapper}{do $global->value = hello}{snippet foo}{$global->value}{/snippet}{/snippetArea}'], ['foo' => 'hello'], '
hello
', ['fooWrapper', 'foo'], ], - //two nested snippets + // two nested snippets [ ['main' => '{snippet foo}{snippet foo2}hello{/snippet}{/snippet}'], ['foo' => '
hello
'], @@ -43,7 +43,7 @@ $dataSets = [ '
hello
', ['foo', 'foo2'], ], - //included template + // included template [ [ 'main' => '{snippet foo}{include file "sub"}{/snippet}', @@ -53,7 +53,7 @@ $dataSets = [ '
hello
', ['foo'], ], - //included template 2 + // included template 2 [ [ 'main' => '{snippet foo}{include file "sub"}{/snippet}', @@ -63,7 +63,7 @@ $dataSets = [ '
hello
', ['foo', 'subFoo'], ], - //included template - expected empty payload + // included template - expected empty payload [ [ 'main' => '{include file "sub"}', @@ -73,7 +73,7 @@ $dataSets = [ '
hello
', ['subFoo'], ], - //included template - snippetArea + // included template - snippetArea [ [ 'main' => '{snippetArea foo}{include file "sub"}{/snippetArea}', @@ -83,7 +83,7 @@ $dataSets = [ '
hello
', ['foo', 'subFoo'], ], - //nested included template - snippetArea + // nested included template - snippetArea [ [ 'main' => '{snippetArea foo}{include file "sub"}{/snippetArea}', @@ -94,7 +94,7 @@ $dataSets = [ '
hello
', ['foo', 'sub2Foo'], ], - //dynamic snippets + // dynamic snippets [ [ 'main' => ' @@ -109,7 +109,7 @@ $dataSets = [ "\n
\n\t\t
1
\n\t\t
2
\n
", ['foo'], ], - //dynamic snippets with a snippetArea + // dynamic snippets with a snippetArea [ [ 'main' => ' @@ -124,7 +124,7 @@ $dataSets = [ "\n\t\t
1
\n\t\t
2
", ['foo'], ], - //snippetArea with a dynamic snippet and included template + // snippetArea with a dynamic snippet and included template [ [ 'main' => ' @@ -140,7 +140,7 @@ $dataSets = [ "\n\t\t
1
\n\t\t
2
", ['foo'], ], - //extends + // extends [ [ 'main' => '{extends "layout"}{block content}{snippet foo}hello{/snippet}', @@ -150,7 +150,7 @@ $dataSets = [ '
hello
world
', ['foo', 'layoutFoo'], ], - //import + // import [ [ 'main' => '{import "blocks1"}{import "blocks2"}{snippet foo}{include block1}{include block2}{/snippet}', @@ -161,7 +161,7 @@ $dataSets = [ '
hello world
', ['foo'], ], - //import and extends + // import and extends [ [ 'main' => '{extends "layout"}{import "blocks1"}{import "blocks2"}{block content}{snippet foo}{include block1}{include block2}{/snippet}', @@ -173,7 +173,7 @@ $dataSets = [ '
hello world
', ['foo'], ], - //embed + // embed [ [ 'main' => '{embed file "embed"}{block embed}{snippet bar}world{/snippet}{/block}{/embed}', @@ -186,7 +186,7 @@ $dataSets = [ ]; foreach ($dataSets as $data) { - //snippet mode + // snippet mode $control = new ControlMock; $control->invalid = array_fill_keys($data[3], value: true); @@ -198,7 +198,7 @@ foreach ($dataSets as $data) { Assert::same($data[1], $control->payload); - //non snippet mode + // non snippet mode $control = new ControlMock; $control->snippetMode = false; $control->invalid = array_fill_keys($data[3], value: true); diff --git a/tests/Bridges.Latte/{link}.2.phpt b/tests/Bridges.Latte/{link}.2.phpt index 7e6626180..72382383f 100644 --- a/tests/Bridges.Latte/{link}.2.phpt +++ b/tests/Bridges.Latte/{link}.2.phpt @@ -82,33 +82,33 @@ Assert::match(<<<'EOD' EOD, strtr($latte->renderToString(<<<'EOD' - {plink Homepage:} + {plink Homepage:} - {plink Homepage: } + {plink Homepage: } - {plink Homepage:action } + {plink Homepage:action } - {plink 'Homepage:action' } + {plink 'Homepage:action' } - {plink Homepage:action 10, 20, '{one}&two'} + {plink Homepage:action 10, 20, '{one}&two'} - {plink Homepage:action#hash 10, 20, '{one}&two'} + {plink Homepage:action#hash 10, 20, '{one}&two'} - {plink #hash} + {plink #hash} - {plink : 10 } + {plink : 10 } - {plink default 10, 'a' => 20, 'b' => 30} + {plink default 10, 'a' => 20, 'b' => 30} - {link $action} + {link $action} - + - + - + - + - - EOD, $params), [''' => "'", ''' => "'", '{' => '{'])); + + EOD, $params), [''' => "'", ''' => "'", '{' => '{'])); diff --git a/tests/Bridges.Tracy/RoutingPanel.analyse.withPath.phpt b/tests/Bridges.Tracy/RoutingPanel.analyse.withPath.phpt index b9ec12926..74d815358 100644 --- a/tests/Bridges.Tracy/RoutingPanel.analyse.withPath.phpt +++ b/tests/Bridges.Tracy/RoutingPanel.analyse.withPath.phpt @@ -32,7 +32,7 @@ test('URL: /', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -45,7 +45,7 @@ test('URL: /', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => null, @@ -53,7 +53,7 @@ test('URL: /', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => null, @@ -63,7 +63,7 @@ test('URL: /', function () use ($router) { ], (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => ['presenter' => 'Article', 'action' => 'homepage'], @@ -71,7 +71,7 @@ test('URL: /', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => null, @@ -95,7 +95,7 @@ test('URL: /foo', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -108,7 +108,7 @@ test('URL: /foo', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => null, @@ -116,7 +116,7 @@ test('URL: /foo', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => null, @@ -126,7 +126,7 @@ test('URL: /foo', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => null, @@ -134,7 +134,7 @@ test('URL: /foo', function () use ($router) { ], (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Foo', 'action' => 'default'], @@ -158,7 +158,7 @@ test('URL: /admin', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -171,7 +171,7 @@ test('URL: /admin', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => ['presenter' => 'Admin:Home', 'action' => 'default'], @@ -179,7 +179,7 @@ test('URL: /admin', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => null, @@ -189,7 +189,7 @@ test('URL: /admin', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => null, @@ -197,7 +197,7 @@ test('URL: /admin', function () use ($router) { ], (object) [ 'matched' => 'may', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Admin', 'action' => 'default'], @@ -221,7 +221,7 @@ test('URL: /admin/', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -234,7 +234,7 @@ test('URL: /admin/', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => ['presenter' => 'Admin:Home', 'action' => 'default'], @@ -242,7 +242,7 @@ test('URL: /admin/', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => null, @@ -252,7 +252,7 @@ test('URL: /admin/', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => null, @@ -260,7 +260,7 @@ test('URL: /admin/', function () use ($router) { ], (object) [ 'matched' => 'may', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Admin', 'action' => 'default'], @@ -284,7 +284,7 @@ test('URL: /admin/foo', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -297,7 +297,7 @@ test('URL: /admin/foo', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => null, @@ -305,7 +305,7 @@ test('URL: /admin/foo', function () use ($router) { ], (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Admin:Foo', 'action' => 'default'], @@ -315,7 +315,7 @@ test('URL: /admin/foo', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => null, @@ -323,7 +323,7 @@ test('URL: /admin/foo', function () use ($router) { ], (object) [ 'matched' => 'may', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Admin', 'action' => 'foo'], diff --git a/tests/Responses/CallbackResponse.phpt b/tests/Responses/CallbackResponse.phpt index e23bbf118..f9cafc264 100644 --- a/tests/Responses/CallbackResponse.phpt +++ b/tests/Responses/CallbackResponse.phpt @@ -8,7 +8,6 @@ use Nette\Application\Responses\CallbackResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Responses/FileResponse.contentDisposition.phpt b/tests/Responses/FileResponse.contentDisposition.phpt index ae712ef00..306f37933 100644 --- a/tests/Responses/FileResponse.contentDisposition.phpt +++ b/tests/Responses/FileResponse.contentDisposition.phpt @@ -8,7 +8,6 @@ use Nette\Application\Responses\FileResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; if (PHP_SAPI === 'cli') { @@ -28,7 +27,10 @@ test('default attachment content disposition', function () { $fileResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); Assert::same($origData, ob_get_clean()); - Assert::same('attachment; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), $response->getHeader('Content-Disposition')); + Assert::same( + 'attachment; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), + $response->getHeader('Content-Disposition'), + ); }); @@ -44,7 +46,10 @@ test('inline content disposition', function () { $fileResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); Assert::same($origData, ob_get_clean()); - Assert::same('inline; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), $response->getHeader('Content-Disposition')); + Assert::same( + 'inline; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), + $response->getHeader('Content-Disposition'), + ); }); @@ -58,5 +63,8 @@ test('filename with special characters', function () { $fileResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); Assert::same($origData, ob_get_clean()); - Assert::same('attachment; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), $response->getHeader('Content-Disposition')); + Assert::same( + 'attachment; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), + $response->getHeader('Content-Disposition'), + ); }); diff --git a/tests/Responses/FileResponse.full.phpt b/tests/Responses/FileResponse.full.phpt index f6e36e186..53790ef4d 100644 --- a/tests/Responses/FileResponse.full.phpt +++ b/tests/Responses/FileResponse.full.phpt @@ -8,7 +8,6 @@ use Nette\Application\Responses\FileResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -29,9 +28,9 @@ test('large file content delivery', function () { $data = ''; for ($i = 0; $i < 20000; $i++) { - $data .= 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi efficitur nisl mauris, ' . - 'sed bibendum leo tempor ornare. Etiam sodales enim sem. Proin lobortis metus at sagittis ' . - 'imperdiet. In non fringilla libero, ac porta purus. Nam gravida quis tellus quis semper.'; + $data .= 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi efficitur nisl mauris, ' + . 'sed bibendum leo tempor ornare. Etiam sodales enim sem. Proin lobortis metus at sagittis ' + . 'imperdiet. In non fringilla libero, ac porta purus. Nam gravida quis tellus quis semper.'; } file_put_contents($file, $data); diff --git a/tests/Responses/FileResponse.range.phpt b/tests/Responses/FileResponse.range.phpt index 6118f4e76..bc7b037a5 100644 --- a/tests/Responses/FileResponse.range.phpt +++ b/tests/Responses/FileResponse.range.phpt @@ -9,7 +9,6 @@ use Nette\Application\Responses\FileResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Responses/JsonResponse.contentType.phpt b/tests/Responses/JsonResponse.contentType.phpt index 3fe4f538b..0ba18460c 100644 --- a/tests/Responses/JsonResponse.contentType.phpt +++ b/tests/Responses/JsonResponse.contentType.phpt @@ -8,7 +8,6 @@ use Nette\Application\Responses\JsonResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; if (PHP_SAPI === 'cli') { @@ -18,7 +17,7 @@ if (PHP_SAPI === 'cli') { test('JSON content type with UTF-8 charset', function () { $data = ['text' => 'žluťoučký kůň']; $encoded = json_encode($data, JSON_UNESCAPED_UNICODE); - $jsonResponse = new JsonResponse($data, 'application/json'); + $jsonResponse = new JsonResponse($data); ob_start(); $jsonResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); @@ -30,7 +29,7 @@ test('JSON content type with UTF-8 charset', function () { test('boolean data JSON encoding', function () { $data = true; $encoded = json_encode($data, JSON_UNESCAPED_UNICODE); - $jsonResponse = new JsonResponse($data, 'application/json'); + $jsonResponse = new JsonResponse($data); ob_start(); $jsonResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); diff --git a/tests/Routers/CliRouter.invalid.phpt b/tests/Routers/CliRouter.invalid.phpt index 71e583f2b..2385d48f0 100644 --- a/tests/Routers/CliRouter.invalid.phpt +++ b/tests/Routers/CliRouter.invalid.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\CliRouter; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/CliRouter.phpt b/tests/Routers/CliRouter.phpt index 669a3f4dd..144b51af9 100644 --- a/tests/Routers/CliRouter.phpt +++ b/tests/Routers/CliRouter.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\CliRouter; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/LinkGenerator.phpt b/tests/Routers/LinkGenerator.phpt index 8fd727ea2..2ae9337f8 100644 --- a/tests/Routers/LinkGenerator.phpt +++ b/tests/Routers/LinkGenerator.phpt @@ -28,6 +28,7 @@ namespace App\Presentation\Module\My { use Nette; + class MyPresenter implements Nette\Application\IPresenter { public function run(Nette\Application\Request $request): Nette\Application\Response @@ -43,7 +44,6 @@ namespace { use Nette\Http; use Tester\Assert; - $pf = new PresenterFactory; @@ -54,7 +54,10 @@ namespace { Assert::same('http://nette.org/en/?presenter=Module%3AMy', $generator->link('Module:My:')); Assert::same('http://nette.org/en/?action=default&presenter=Homepage', $generator->link('Homepage:')); Assert::same('http://nette.org/en/?a=10&action=default&presenter=Homepage', $generator->link('Homepage:', [10])); - Assert::same('http://nette.org/en/?id=20&b=10&action=detail&presenter=Homepage', $generator->link('Homepage:detail', [10, 'id' => 20])); + Assert::same( + 'http://nette.org/en/?id=20&b=10&action=detail&presenter=Homepage', + $generator->link('Homepage:detail', [10, 'id' => 20]), + ); Assert::same('http://nette.org/en/?action=default&presenter=Homepage#frag:ment', $generator->link('Homepage:#frag:ment')); Assert::same('http://nette.org/en/?id=10&action=missing&presenter=Homepage', $generator->link('Homepage:missing', ['id' => 10])); }); @@ -85,7 +88,10 @@ namespace { Assert::same('http://nette.org/en/?presenter=Module%3AMy', $generator->link('Module:My:')); Assert::same('http://nette.org/en/?presenter=Homepage', $generator->link('Homepage:')); Assert::same('http://nette.org/en/?0=10&presenter=Homepage', $generator->link('Homepage:', [10])); - Assert::same('http://nette.org/en/?0=10&id=20&action=detail&presenter=Homepage', $generator->link('Homepage:detail', [10, 'id' => 20])); + Assert::same( + 'http://nette.org/en/?0=10&id=20&action=detail&presenter=Homepage', + $generator->link('Homepage:detail', [10, 'id' => 20]), + ); Assert::same('http://nette.org/en/?presenter=Homepage#frag:ment', $generator->link('Homepage:#frag:ment')); Assert::same('http://nette.org/en/?action=default&presenter=Homepage', $generator->link('Homepage:default')); }); diff --git a/tests/Routers/Route.arrayParams.phpt b/tests/Routers/Route.arrayParams.phpt index fa3711e49..6de4d776b 100644 --- a/tests/Routers/Route.arrayParams.phpt +++ b/tests/Routers/Route.arrayParams.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.autooptional1.phpt b/tests/Routers/Route.autooptional1.phpt index a4b2cc751..091e5bb63 100644 --- a/tests/Routers/Route.autooptional1.phpt +++ b/tests/Routers/Route.autooptional1.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.autooptional2.phpt b/tests/Routers/Route.autooptional2.phpt index 5d4fab1aa..d75e27adc 100644 --- a/tests/Routers/Route.autooptional2.phpt +++ b/tests/Routers/Route.autooptional2.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.autooptional3.phpt b/tests/Routers/Route.autooptional3.phpt index 802811ce4..82d30c9d3 100644 --- a/tests/Routers/Route.autooptional3.phpt +++ b/tests/Routers/Route.autooptional3.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.autooptional4.phpt b/tests/Routers/Route.autooptional4.phpt index 861a830ec..6801edf58 100644 --- a/tests/Routers/Route.autooptional4.phpt +++ b/tests/Routers/Route.autooptional4.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.basic.phpt b/tests/Routers/Route.basic.phpt index a8c7d0917..316626cdc 100644 --- a/tests/Routers/Route.basic.phpt +++ b/tests/Routers/Route.basic.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.camelcapsVsDash.phpt b/tests/Routers/Route.camelcapsVsDash.phpt index 2f1502188..8287deaab 100644 --- a/tests/Routers/Route.camelcapsVsDash.phpt +++ b/tests/Routers/Route.camelcapsVsDash.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.closure.phpt b/tests/Routers/Route.closure.phpt index 50a01ebf0..366ae225e 100644 --- a/tests/Routers/Route.closure.phpt +++ b/tests/Routers/Route.closure.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.combinedUrlParam.phpt b/tests/Routers/Route.combinedUrlParam.phpt index d9a8dce6c..d987f95f3 100644 --- a/tests/Routers/Route.combinedUrlParam.phpt +++ b/tests/Routers/Route.combinedUrlParam.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.errors.phpt b/tests/Routers/Route.errors.phpt index fa7a3a76c..ee905d65b 100644 --- a/tests/Routers/Route.errors.phpt +++ b/tests/Routers/Route.errors.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/Route.extraDefaultParam.phpt b/tests/Routers/Route.extraDefaultParam.phpt index 22d60174c..3e73456d2 100644 --- a/tests/Routers/Route.extraDefaultParam.phpt +++ b/tests/Routers/Route.extraDefaultParam.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filter.global.phpt b/tests/Routers/Route.filter.global.phpt index 335474922..b64514dd6 100644 --- a/tests/Routers/Route.filter.global.phpt +++ b/tests/Routers/Route.filter.global.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; @@ -16,7 +15,7 @@ require __DIR__ . '/Route.php'; $route = new Route('', [ '' => [ Route::FilterIn => function (array $arr) { - if (substr($arr['presenter'], 0, 3) !== 'Abc') { + if (!str_starts_with($arr['presenter'], 'Abc')) { return null; } @@ -25,7 +24,7 @@ $route = new Route('', [ return $arr; }, Route::FilterOut => function (array $arr) { - if (substr($arr['presenter'], 0, 3) !== 'Abc') { + if (!str_starts_with($arr['presenter'], 'Abc')) { return null; } diff --git a/tests/Routers/Route.filter.query.phpt b/tests/Routers/Route.filter.query.phpt index 7dd39e1a4..7ae708987 100644 --- a/tests/Routers/Route.filter.query.phpt +++ b/tests/Routers/Route.filter.query.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filter.url.object.phpt b/tests/Routers/Route.filter.url.object.phpt index 69868c304..1128502a7 100644 --- a/tests/Routers/Route.filter.url.object.phpt +++ b/tests/Routers/Route.filter.url.object.phpt @@ -7,8 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filter.url.phpt b/tests/Routers/Route.filter.url.phpt index c533e3880..f9fe7028c 100644 --- a/tests/Routers/Route.filter.url.phpt +++ b/tests/Routers/Route.filter.url.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filterTable.query.phpt b/tests/Routers/Route.filterTable.query.phpt index 122d1e4a0..511528117 100644 --- a/tests/Routers/Route.filterTable.query.phpt +++ b/tests/Routers/Route.filterTable.query.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filterTable.strict.phpt b/tests/Routers/Route.filterTable.strict.phpt index c7509c9f3..be618923b 100644 --- a/tests/Routers/Route.filterTable.strict.phpt +++ b/tests/Routers/Route.filterTable.strict.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filterTable.url.phpt b/tests/Routers/Route.filterTable.url.phpt index daab4cf3d..c930f4f03 100644 --- a/tests/Routers/Route.filterTable.url.phpt +++ b/tests/Routers/Route.filterTable.url.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.firstOptionalParam.phpt b/tests/Routers/Route.firstOptionalParam.phpt index fa4ad1ece..4b9ee6e2e 100644 --- a/tests/Routers/Route.firstOptionalParam.phpt +++ b/tests/Routers/Route.firstOptionalParam.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.fooParameter.default.phpt b/tests/Routers/Route.fooParameter.default.phpt index 235e29298..6f93d0379 100644 --- a/tests/Routers/Route.fooParameter.default.phpt +++ b/tests/Routers/Route.fooParameter.default.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.fooParameter.phpt b/tests/Routers/Route.fooParameter.phpt index 6d1a0de85..ccf62c6b9 100644 --- a/tests/Routers/Route.fooParameter.phpt +++ b/tests/Routers/Route.fooParameter.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.fullMatchParam.phpt b/tests/Routers/Route.fullMatchParam.phpt index 399e0bd1d..a1afef570 100644 --- a/tests/Routers/Route.fullMatchParam.phpt +++ b/tests/Routers/Route.fullMatchParam.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.longParameter.phpt b/tests/Routers/Route.longParameter.phpt index e5de099b6..a4492c3ef 100644 --- a/tests/Routers/Route.longParameter.phpt +++ b/tests/Routers/Route.longParameter.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.mandatoryAction.phpt b/tests/Routers/Route.mandatoryAction.phpt index 5e887060c..285d53473 100644 --- a/tests/Routers/Route.mandatoryAction.phpt +++ b/tests/Routers/Route.mandatoryAction.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.modules.fixity.phpt b/tests/Routers/Route.modules.fixity.phpt index e133ebb18..01b239d4e 100644 --- a/tests/Routers/Route.modules.fixity.phpt +++ b/tests/Routers/Route.modules.fixity.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/Route.modules.phpt b/tests/Routers/Route.modules.phpt index 57e892268..94a3d14d2 100644 --- a/tests/Routers/Route.modules.phpt +++ b/tests/Routers/Route.modules.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.noDefaultParams.phpt b/tests/Routers/Route.noDefaultParams.phpt index 2533be317..fb2351adc 100644 --- a/tests/Routers/Route.noDefaultParams.phpt +++ b/tests/Routers/Route.noDefaultParams.phpt @@ -6,14 +6,12 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; -$route = new Route('//', [ -]); +$route = new Route('//'); testRouteIn($route, '/presenter/action/12', [ 'presenter' => 'Presenter', diff --git a/tests/Routers/Route.noParams.phpt b/tests/Routers/Route.noParams.phpt index 68c9ecce7..4adab93ce 100644 --- a/tests/Routers/Route.noParams.phpt +++ b/tests/Routers/Route.noParams.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.autooptional1.phpt b/tests/Routers/Route.optional.autooptional1.phpt index f3a39f278..e06932f29 100644 --- a/tests/Routers/Route.optional.autooptional1.phpt +++ b/tests/Routers/Route.optional.autooptional1.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.autooptional2.phpt b/tests/Routers/Route.optional.autooptional2.phpt index 56304fd09..e35a4d203 100644 --- a/tests/Routers/Route.optional.autooptional2.phpt +++ b/tests/Routers/Route.optional.autooptional2.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.autooptional3.phpt b/tests/Routers/Route.optional.autooptional3.phpt index c8ee32c75..7e3fd5760 100644 --- a/tests/Routers/Route.optional.autooptional3.phpt +++ b/tests/Routers/Route.optional.autooptional3.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.module.phpt b/tests/Routers/Route.optional.module.phpt index 578a051be..9b8c5faa4 100644 --- a/tests/Routers/Route.optional.module.phpt +++ b/tests/Routers/Route.optional.module.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.nested.phpt b/tests/Routers/Route.optional.nested.phpt index ed2a00548..1fcf61a36 100644 --- a/tests/Routers/Route.optional.nested.phpt +++ b/tests/Routers/Route.optional.nested.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.phpt b/tests/Routers/Route.optional.phpt index d17e7311d..2e302d56f 100644 --- a/tests/Routers/Route.optional.phpt +++ b/tests/Routers/Route.optional.phpt @@ -6,14 +6,12 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; -$route = new Route('index[.html]', [ -]); +$route = new Route('index[.html]'); testRouteIn($route, '/index.html', [ 'presenter' => 'querypresenter', diff --git a/tests/Routers/Route.optional.precedence.phpt b/tests/Routers/Route.optional.precedence.phpt index fc978779f..1fcb09d22 100644 --- a/tests/Routers/Route.optional.precedence.phpt +++ b/tests/Routers/Route.optional.precedence.phpt @@ -6,14 +6,12 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; -$route = new Route('[/][]', [ -]); +$route = new Route('[/][]'); testRouteIn($route, '/one', [ 'presenter' => 'querypresenter', diff --git a/tests/Routers/Route.optional.preferred.nested1.phpt b/tests/Routers/Route.optional.preferred.nested1.phpt index 44d13b9a8..f7e6eed53 100644 --- a/tests/Routers/Route.optional.preferred.nested1.phpt +++ b/tests/Routers/Route.optional.preferred.nested1.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.preferred.nested2.phpt b/tests/Routers/Route.optional.preferred.nested2.phpt index 004600464..0930e985f 100644 --- a/tests/Routers/Route.optional.preferred.nested2.phpt +++ b/tests/Routers/Route.optional.preferred.nested2.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.preferred.nested3.phpt b/tests/Routers/Route.optional.preferred.nested3.phpt index e7033874a..83a3d454b 100644 --- a/tests/Routers/Route.optional.preferred.nested3.phpt +++ b/tests/Routers/Route.optional.preferred.nested3.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.preferred.phpt b/tests/Routers/Route.optional.preferred.phpt index 5da5178c2..0a5922705 100644 --- a/tests/Routers/Route.optional.preferred.phpt +++ b/tests/Routers/Route.optional.preferred.phpt @@ -6,14 +6,12 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; -$route = new Route('index[!.html]', [ -]); +$route = new Route('index[!.html]'); testRouteIn($route, '/index.html', [ 'presenter' => 'querypresenter', diff --git a/tests/Routers/Route.optional.regexp.phpt b/tests/Routers/Route.optional.regexp.phpt index 970a14c27..d32d29ab9 100644 --- a/tests/Routers/Route.optional.regexp.phpt +++ b/tests/Routers/Route.optional.regexp.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.scalarParams.phpt b/tests/Routers/Route.scalarParams.phpt index f951c161b..15ddbdca1 100644 --- a/tests/Routers/Route.scalarParams.phpt +++ b/tests/Routers/Route.scalarParams.phpt @@ -7,15 +7,13 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; test('numeric and boolean parameter serialization', function () { - $route = new Route('/', [ - ]); + $route = new Route('/'); Assert::same( 'http://example.com/homepage/12', diff --git a/tests/Routers/Route.secured.phpt b/tests/Routers/Route.secured.phpt index 70dac8abe..e3eb9d3ed 100644 --- a/tests/Routers/Route.secured.phpt +++ b/tests/Routers/Route.secured.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\Route; use Nette\Http\UrlScript; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.slash.phpt b/tests/Routers/Route.slash.phpt index 20a32ee68..35e13401b 100644 --- a/tests/Routers/Route.slash.phpt +++ b/tests/Routers/Route.slash.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.type.phpt b/tests/Routers/Route.type.phpt index 9d4577a9a..5b110c2d3 100644 --- a/tests/Routers/Route.type.phpt +++ b/tests/Routers/Route.type.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.urlEncoding.phpt b/tests/Routers/Route.urlEncoding.phpt index f554e4c4b..f56e9f947 100644 --- a/tests/Routers/Route.urlEncoding.phpt +++ b/tests/Routers/Route.urlEncoding.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.utf8Param.phpt b/tests/Routers/Route.utf8Param.phpt index bc905d16a..d83f4c2db 100644 --- a/tests/Routers/Route.utf8Param.phpt +++ b/tests/Routers/Route.utf8Param.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.variables.phpt b/tests/Routers/Route.variables.phpt index d5edae7b1..dca11d2bb 100644 --- a/tests/Routers/Route.variables.phpt +++ b/tests/Routers/Route.variables.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withAbsolutePath.phpt b/tests/Routers/Route.withAbsolutePath.phpt index 41368727c..2dd6130c3 100644 --- a/tests/Routers/Route.withAbsolutePath.phpt +++ b/tests/Routers/Route.withAbsolutePath.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withDefaultPresenterAndAction.phpt b/tests/Routers/Route.withDefaultPresenterAndAction.phpt index 5c36fed10..e421c03df 100644 --- a/tests/Routers/Route.withDefaultPresenterAndAction.phpt +++ b/tests/Routers/Route.withDefaultPresenterAndAction.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withHost.phpt b/tests/Routers/Route.withHost.phpt index 4d427dfdc..37da50316 100644 --- a/tests/Routers/Route.withHost.phpt +++ b/tests/Routers/Route.withHost.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withHost.protocol.phpt b/tests/Routers/Route.withHost.protocol.phpt index d1a3f7d3b..16896ad70 100644 --- a/tests/Routers/Route.withHost.protocol.phpt +++ b/tests/Routers/Route.withHost.protocol.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withHost.secured.phpt b/tests/Routers/Route.withHost.secured.phpt index 0995bc3d3..29d0fe377 100644 --- a/tests/Routers/Route.withHost.secured.phpt +++ b/tests/Routers/Route.withHost.secured.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\Route; use Nette\Http\UrlScript; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withNamedParamsInQuery.phpt b/tests/Routers/Route.withNamedParamsInQuery.phpt index e28e19b3f..16d8b8a16 100644 --- a/tests/Routers/Route.withNamedParamsInQuery.phpt +++ b/tests/Routers/Route.withNamedParamsInQuery.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withParamsInQuery.phpt b/tests/Routers/Route.withParamsInQuery.phpt index e99626ca7..5d9b83175 100644 --- a/tests/Routers/Route.withParamsInQuery.phpt +++ b/tests/Routers/Route.withParamsInQuery.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withUserClassAlt.phpt b/tests/Routers/Route.withUserClassAlt.phpt index 8abccd6f6..80fd83d42 100644 --- a/tests/Routers/Route.withUserClassAlt.phpt +++ b/tests/Routers/Route.withUserClassAlt.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/RouteList.addRoute.phpt b/tests/Routers/RouteList.addRoute.phpt index 1e7dab56c..e41357e3f 100644 --- a/tests/Routers/RouteList.addRoute.phpt +++ b/tests/Routers/RouteList.addRoute.phpt @@ -2,15 +2,14 @@ use Nette\Application\Routers\RouteList; - require __DIR__ . '/../bootstrap.php'; 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('foo', ['presenter' => 'foo'], oneWay: 1); +$list->addRoute('bar', ['presenter' => 'bar'], oneWay: 1); $list->addRoute('hello', ['presenter' => 'hello']); diff --git a/tests/Routers/RouteList.basic.phpt b/tests/Routers/RouteList.basic.phpt index 65d1b1ea2..cadac1071 100644 --- a/tests/Routers/RouteList.basic.phpt +++ b/tests/Routers/RouteList.basic.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\RouteList; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/RouteList.modules.phpt b/tests/Routers/RouteList.modules.phpt index 9b34ccfcd..2bc3cfd11 100644 --- a/tests/Routers/RouteList.modules.phpt +++ b/tests/Routers/RouteList.modules.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\RouteList; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/RouteList.withModule.phpt b/tests/Routers/RouteList.withModule.phpt index 4e77d0859..1c872fcdb 100644 --- a/tests/Routers/RouteList.withModule.phpt +++ b/tests/Routers/RouteList.withModule.phpt @@ -2,7 +2,6 @@ use Nette\Application\Routers\RouteList; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/SimpleRouter.invalid.phpt b/tests/Routers/SimpleRouter.invalid.phpt index f9bd9881a..2f63a7498 100644 --- a/tests/Routers/SimpleRouter.invalid.phpt +++ b/tests/Routers/SimpleRouter.invalid.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\SimpleRouter; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/SimpleRouter.phpt b/tests/Routers/SimpleRouter.phpt index 2f20ad085..dd3bd62d2 100644 --- a/tests/Routers/SimpleRouter.phpt +++ b/tests/Routers/SimpleRouter.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\SimpleRouter; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Component.isLinkCurrent().asserts.php b/tests/UI/Component.isLinkCurrent().asserts.php index 62ae5b39b..82efda54f 100644 --- a/tests/UI/Component.isLinkCurrent().asserts.php +++ b/tests/UI/Component.isLinkCurrent().asserts.php @@ -33,7 +33,7 @@ function callIsComponentLinkCurrent( $presenterFactory, new Application\Routers\SimpleRouter, ); - $presenter->onStartup[] = function () use (&$res, $component, $destination, $args) { + $presenter->onStartup[] = function () use (&$res, $component, $destination, $args) { $res = $component->isLinkCurrent($destination, $args); }; $presenter->run($request); diff --git a/tests/UI/Component.redirect().phpt b/tests/UI/Component.redirect().phpt index b5cf7101b..7fe067d35 100644 --- a/tests/UI/Component.redirect().phpt +++ b/tests/UI/Component.redirect().phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Component.signals.phpt b/tests/UI/Component.signals.phpt index a3e37c1e9..24dc3e457 100644 --- a/tests/UI/Component.signals.phpt +++ b/tests/UI/Component.signals.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/ComponentReflection.parseAnnotation.phpt b/tests/UI/ComponentReflection.parseAnnotation.phpt index d9151d743..ad03beeea 100644 --- a/tests/UI/ComponentReflection.parseAnnotation.phpt +++ b/tests/UI/ComponentReflection.parseAnnotation.phpt @@ -7,7 +7,6 @@ use Nette\Application\UI\ComponentReflection as Reflection; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -32,11 +31,11 @@ class TestClass $rc = new ReflectionClass('TestClass'); -Assert::same(['value ="Johno\'s addendum"', 'mode=True', true, true], @Reflection::parseAnnotation($rc, 'title')); -Assert::null(@Reflection::parseAnnotation($rc, 'public')); -Assert::null(@Reflection::parseAnnotation($rc, 'private')); -Assert::same(['item 1'], @Reflection::parseAnnotation($rc, 'components')); -Assert::same([true, false, null], @Reflection::parseAnnotation($rc, 'persistent')); -Assert::same([true], @Reflection::parseAnnotation($rc, 'renderable')); -Assert::same(['loggedIn'], @Reflection::parseAnnotation($rc, 'Secured\User')); -Assert::null(@Reflection::parseAnnotation($rc, 'missing')); +Assert::same(['value ="Johno\'s addendum"', 'mode=True', true, true], @Reflection::parseAnnotation($rc, 'title')); // @ annotations are deprecated +Assert::null(Reflection::parseAnnotation($rc, 'public')); +Assert::null(Reflection::parseAnnotation($rc, 'private')); +Assert::same(['item 1'], @Reflection::parseAnnotation($rc, 'components')); // @ annotations are deprecated +Assert::same([true, false, null], @Reflection::parseAnnotation($rc, 'persistent')); // @ annotations are deprecated +Assert::same([true], @Reflection::parseAnnotation($rc, 'renderable')); // @ annotations are deprecated +Assert::same(['loggedIn'], @Reflection::parseAnnotation($rc, 'Secured\User')); // @ annotations are deprecated +Assert::null(Reflection::parseAnnotation($rc, 'missing')); diff --git a/tests/UI/Control.formatTemplateClass.phpt b/tests/UI/Control.formatTemplateClass.phpt index 2da3e7979..e705fe26e 100644 --- a/tests/UI/Control.formatTemplateClass.phpt +++ b/tests/UI/Control.formatTemplateClass.phpt @@ -6,7 +6,6 @@ use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Control.isControlInvalid.phpt b/tests/UI/Control.isControlInvalid.phpt index 9de0121fe..41ed2c63c 100644 --- a/tests/UI/Control.isControlInvalid.phpt +++ b/tests/UI/Control.isControlInvalid.phpt @@ -7,7 +7,6 @@ use Nette\Application\UI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Deprecated.link().phpt b/tests/UI/Deprecated.link().phpt index 5252b10b8..676dab93e 100644 --- a/tests/UI/Deprecated.link().phpt +++ b/tests/UI/Deprecated.link().phpt @@ -61,7 +61,7 @@ $presenter->injectPrimary( $presenter->invalidLinkMode = TestPresenter::InvalidLinkWarning; -$request = new Application\Request('Test', Http\Request::Get, []); +$request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); Assert::error( diff --git a/tests/UI/Form.phpt b/tests/UI/Form.phpt index bae67de9f..c21b8207c 100644 --- a/tests/UI/Form.phpt +++ b/tests/UI/Form.phpt @@ -7,7 +7,6 @@ use Nette\Application\UI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/LinkGenerator.parseDestination().phpt b/tests/UI/LinkGenerator.parseDestination().phpt index c3dc3faa8..a6e0a5614 100644 --- a/tests/UI/LinkGenerator.parseDestination().phpt +++ b/tests/UI/LinkGenerator.parseDestination().phpt @@ -4,7 +4,6 @@ use Nette\Application\LinkGenerator; use Nette\Application\UI\InvalidLinkException; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -46,7 +45,7 @@ Assert::same([ 'signal' => false, 'args' => [], 'fragment' => '#fragment', -], @LinkGenerator::parseDestination('a:b?#fragment')); // deprecated +], @LinkGenerator::parseDestination('a:b?#fragment')); // @ query string in link is deprecated Assert::same([ 'absolute' => false, @@ -54,7 +53,7 @@ Assert::same([ 'signal' => false, 'args' => ['a' => 'b', 'c' => 'd'], 'fragment' => '#fragment', -], @LinkGenerator::parseDestination('a:b?a=b&c=d#fragment')); // deprecated +], @LinkGenerator::parseDestination('a:b?a=b&c=d#fragment')); // @ query string in link is deprecated Assert::exception( fn() => LinkGenerator::parseDestination(''), diff --git a/tests/UI/Multiplier.phpt b/tests/UI/Multiplier.phpt index 3496de3b6..0628b9896 100644 --- a/tests/UI/Multiplier.phpt +++ b/tests/UI/Multiplier.phpt @@ -7,7 +7,6 @@ use Nette\Application; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/ParameterConverter.convertType.phpt b/tests/UI/ParameterConverter.convertType.phpt index eae59e8e6..e3406f026 100644 --- a/tests/UI/ParameterConverter.convertType.phpt +++ b/tests/UI/ParameterConverter.convertType.phpt @@ -7,7 +7,6 @@ use Nette\Application\UI\ParameterConverter; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Presenter.canonicalize.phpt b/tests/UI/Presenter.canonicalize.phpt index f9947d8fa..26dd1ef5a 100644 --- a/tests/UI/Presenter.canonicalize.phpt +++ b/tests/UI/Presenter.canonicalize.phpt @@ -8,7 +8,6 @@ use Nette\{Application, Http}; use Nette\Application\Attributes\Persistent; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/functions.php'; @@ -173,7 +172,7 @@ test('canonicalize() uses 302 for varying requests', function () { $presenter->autoCanonicalize = true; $request = new Application\Request('Varying', 'GET', ['action' => 'default']); - $request->setFlag($request::VARYING, true); + $request->setFlag($request::VARYING); $response = $presenter->run($request); diff --git a/tests/UI/Presenter.flashMessage.phpt b/tests/UI/Presenter.flashMessage.phpt index 6671cf51d..3cb5bac87 100644 --- a/tests/UI/Presenter.flashMessage.phpt +++ b/tests/UI/Presenter.flashMessage.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/functions.php'; @@ -129,7 +128,7 @@ test('multiple flash messages are stored', function () { $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); - $presenter->flashMessage('First message', 'info'); + $presenter->flashMessage('First message'); $presenter->flashMessage('Second message', 'error'); $presenter->flashMessage('Third message', 'success'); @@ -186,10 +185,13 @@ test('flash session expires after presenter run', function () { $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; - }); + $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); diff --git a/tests/UI/Presenter.formatLayoutTemplateFiles.phpt b/tests/UI/Presenter.formatLayoutTemplateFiles.phpt index d7f81ba01..792e51693 100644 --- a/tests/UI/Presenter.formatLayoutTemplateFiles.phpt +++ b/tests/UI/Presenter.formatLayoutTemplateFiles.phpt @@ -6,7 +6,6 @@ use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/one/APresenter.php'; require __DIR__ . '/one/sub/BPresenter.php'; diff --git a/tests/UI/Presenter.formatTemplateClass.phpt b/tests/UI/Presenter.formatTemplateClass.phpt index 68eebb781..115c65e71 100644 --- a/tests/UI/Presenter.formatTemplateClass.phpt +++ b/tests/UI/Presenter.formatTemplateClass.phpt @@ -6,7 +6,6 @@ use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Presenter.formatTemplateFiles.phpt b/tests/UI/Presenter.formatTemplateFiles.phpt index 8031f366f..e79b06f89 100644 --- a/tests/UI/Presenter.formatTemplateFiles.phpt +++ b/tests/UI/Presenter.formatTemplateFiles.phpt @@ -6,7 +6,6 @@ use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/one/APresenter.php'; require __DIR__ . '/one/sub/BPresenter.php'; diff --git a/tests/UI/Presenter.initGlobalParameters().phpt b/tests/UI/Presenter.initGlobalParameters().phpt index 3caee4d3d..bed395da4 100644 --- a/tests/UI/Presenter.initGlobalParameters().phpt +++ b/tests/UI/Presenter.initGlobalParameters().phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Presenter.lifecycle.phpt b/tests/UI/Presenter.lifecycle.phpt index c1b3d38a4..b68e448ff 100644 --- a/tests/UI/Presenter.lifecycle.phpt +++ b/tests/UI/Presenter.lifecycle.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/functions.php'; diff --git a/tests/UI/Presenter.link().persistent.phpt b/tests/UI/Presenter.link().persistent.phpt index ee4f24348..4a65954f3 100644 --- a/tests/UI/Presenter.link().persistent.phpt +++ b/tests/UI/Presenter.link().persistent.phpt @@ -8,7 +8,6 @@ use Nette\{Application, Http}; use Nette\Application\Attributes\Persistent; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -161,6 +160,6 @@ test('Presenter link() with persistent parameters', function () { $presenter->invalidLinkMode = TestPresenter::InvalidLinkWarning; $presenter->autoCanonicalize = false; - $request = new Application\Request('Test', Http\Request::Get, []); + $request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); }); diff --git a/tests/UI/Presenter.link().phpt b/tests/UI/Presenter.link().phpt index 74ca7e02e..ca417fdb3 100644 --- a/tests/UI/Presenter.link().phpt +++ b/tests/UI/Presenter.link().phpt @@ -8,7 +8,6 @@ use Nette\{Application, Http}; use Nette\Application\Attributes\Persistent; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -76,17 +75,46 @@ class TestPresenter extends Application\UI\Presenter // standard Assert::same("#error: Invalid destination ''.", $this->link('')); Assert::same('/index.php?action=params&presenter=Test', $this->link('params')); - Assert::same(['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same( + ['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); Assert::same('http://localhost/index.php?action=params&presenter=Test', $this->link('//params')); - Assert::same('#error: Argument $arr passed to TestPresenter::actionParams() must be scalar, array given.', $this->link('params', [1, 2, 3, []])); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionParams() must be scalar, array given.', + $this->link('params', [1, 2, 3, []]), + ); Assert::same('/index.php?xx=1&action=params&presenter=Test', $this->link('params', ['xx' => 1, 'yy' => []])); - Assert::same(['xx' => 1, 'yy' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?sort%5By%5D%5Basc%5D=1&action=default&presenter=Test', $this->link('this', ['sort' => ['y' => ['asc' => true]]])); - Assert::same(['sort' => ['y' => ['asc' => true]], 'pint' => null, 'parr' => null, 'pbool' => null, 'mycontrol-order' => null, 'mycontrol-round' => null, 'action' => 'default'], $this->getLastCreatedRequest()->getParameters()); + Assert::same( + ['xx' => 1, 'yy' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?sort%5By%5D%5Basc%5D=1&action=default&presenter=Test', + $this->link('this', ['sort' => ['y' => ['asc' => true]]]), + ); + Assert::same( + [ + 'sort' => ['y' => ['asc' => true]], + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'mycontrol-order' => null, + 'mycontrol-round' => null, + 'action' => 'default', + ], + $this->getLastCreatedRequest()->getParameters(), + ); - Assert::same("#error: Unable to pass parameters to action 'Test:product', missing corresponding method TestPresenter::renderProduct().", $this->link('product', 1)); + Assert::same( + "#error: Unable to pass parameters to action 'Test:product', missing corresponding method TestPresenter::renderProduct().", + $this->link('product', 1), + ); 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)); + Assert::same( + '#error: Passed more parameters than method TestPresenter::actionParams() expects.', + $this->link('params', 1, 2, 3, 4, 5), + ); // fragment via args Assert::same('/index.php?action=product&presenter=Test#fragment', $this->link('product', ['#' => 'fragment'])); @@ -96,7 +124,10 @@ class TestPresenter extends Application\UI\Presenter Assert::same('/index.php?action=product&presenter=Test', $this->link('product', ['#' => ''])); // empty fragment omitted Assert::same('/index.php?action=product&presenter=Test', $this->link('product', ['#' => null])); Assert::same(['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'product'], $this->getLastCreatedRequest()->getParameters()); // # not propagated to request - Assert::same('http://localhost/index.php?x=1&action=product&presenter=Test#fragment', $this->link('//product', ['x' => 1, '#' => 'fragment'])); + Assert::same( + 'http://localhost/index.php?x=1&action=product&presenter=Test#fragment', + $this->link('//product', ['x' => 1, '#' => 'fragment']), + ); Assert::same('/index.php?action=product&presenter=Test#hello%20world%20%26%20co', $this->link('product', ['#' => 'hello world & co'])); // URL encoding Assert::same('/index.php?action=product&presenter=Test#42', $this->link('product', ['#' => 42])); // scalar coercion Assert::same("#error: Value of '#' must be scalar, array given.", $this->link('product', ['#' => [1, 2]])); @@ -105,28 +136,67 @@ class TestPresenter extends Application\UI\Presenter Assert::same('/index.php?action=product&presenter=Test#frag', (string) new Application\UI\Link($this, 'product', ['#' => 'frag'])); // Link class // absolute - 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 = true; - 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])); + 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 Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['pint' => $this->pint, 'p' => ''])); - Assert::same('/index.php?pint=20&parr%5B0%5D=1&action=params&presenter=Test', $this->link('params', ['pint' => $this->pint * 2, 'pbool' => true, 'parr' => [1]])); + Assert::same( + '/index.php?pint=20&parr%5B0%5D=1&action=params&presenter=Test', + $this->link('params', ['pint' => $this->pint * 2, 'pbool' => true, 'parr' => [1]]), + ); Assert::same(['pint' => 20, 'pbool' => null, 'parr' => [1], 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?pint=1&pbool=0&action=params&presenter=Test', $this->link('params', ['pint' => true, 'pbool' => '0', 'parr' => []])); - Assert::same('/index.php?pint=0&pbool=0&p=0&action=params&presenter=Test', $this->link('params', ['pint' => false, 'pbool' => false, 'p' => false, 'parr' => null])); - Assert::same("#error: Value passed to persistent parameter 'pbool' in presenter Test must be bool, string given.", $this->link('this', ['p' => null, 'pbool' => 'a'])); - Assert::same("#error: Value passed to persistent parameter 'p' in presenter Test must be scalar, array given.", $this->link('this', ['p' => [1], 'pbool' => false])); + Assert::same( + '/index.php?pint=1&pbool=0&action=params&presenter=Test', + $this->link('params', ['pint' => true, 'pbool' => '0', 'parr' => []]), + ); + Assert::same( + '/index.php?pint=0&pbool=0&p=0&action=params&presenter=Test', + $this->link('params', ['pint' => false, 'pbool' => false, 'p' => false, 'parr' => null]), + ); + Assert::same( + "#error: Value passed to persistent parameter 'pbool' in presenter Test must be bool, string given.", + $this->link('this', ['p' => null, 'pbool' => 'a']), + ); + Assert::same( + "#error: Value passed to persistent parameter 'p' in presenter Test must be scalar, array given.", + $this->link('this', ['p' => [1], 'pbool' => false]), + ); Assert::same('/index.php?action=persistent&presenter=Test', $this->link('persistent')); - Assert::same('/index.php?pbooln=1&parrn%5B0%5D=1&action=params&presenter=Test', $this->link('params', ['pbooln' => true, 'parrn' => [1]])); - Assert::same(['pbooln' => true, 'parrn' => [1], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same( + '/index.php?pbooln=1&parrn%5B0%5D=1&action=params&presenter=Test', + $this->link('params', ['pbooln' => true, 'parrn' => [1]]), + ); + Assert::same( + ['pbooln' => true, 'parrn' => [1], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); Assert::same('/index.php?pbooln=0&action=params&presenter=Test', $this->link('params', ['pbooln' => '0', 'parrn' => []])); - Assert::same(['pbooln' => false, 'parrn' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same("#error: Value passed to persistent parameter 'pbooln' in presenter Test must be ?bool, string given.", $this->link('params', ['pbooln' => 'a'])); - Assert::same("#error: Value passed to persistent parameter 'parrn' in presenter Test must be array, string given.", $this->link('params', ['parrn' => 'a'])); + Assert::same( + ['pbooln' => false, 'parrn' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + "#error: Value passed to persistent parameter 'pbooln' in presenter Test must be ?bool, string given.", + $this->link('params', ['pbooln' => 'a']), + ); + Assert::same( + "#error: Value passed to persistent parameter 'parrn' in presenter Test must be array, string given.", + $this->link('params', ['parrn' => 'a']), + ); // Other presenter & action link Assert::same('/index.php?action=product&presenter=Other', $this->link('Other:product', ['p' => $this->p])); @@ -141,94 +211,330 @@ class TestPresenter extends Application\UI\Presenter Assert::same('/index.php?action=default&do=signal&presenter=Test', $this->link('signal!', ['p' => $this->p])); Assert::same('/index.php?p=0&action=default&do=signal&presenter=Test', $this->link('signal!', [1, 'p' => $this->p * 2])); Assert::same('/index.php?y=2&action=default&do=signal&presenter=Test', $this->link('signal!', 1, 2)); - Assert::same(['x' => null, 'y' => 2, 'pint' => null, 'parr' => null, 'pbool' => null, 'mycontrol-order' => null, 'mycontrol-round' => null, 'action' => 'default', 'do' => 'signal'], $this->getLastCreatedRequest()->getParameters()); + Assert::same( + [ + 'x' => null, + 'y' => 2, + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'mycontrol-order' => null, + 'mycontrol-round' => null, + 'action' => 'default', + 'do' => 'signal', + ], + $this->getLastCreatedRequest()->getParameters(), + ); // Component link Assert::same("#error: Invalid destination ''.", $this['mycontrol']->link('', 0, 1)); - Assert::same('/index.php?mycontrol-x=0&mycontrol-y=1&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', 0, 1)); - Assert::same('/index.php?mycontrol-x=0a&mycontrol-y=1a&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', '0a', '1a')); + Assert::same( + '/index.php?mycontrol-x=0&mycontrol-y=1&action=default&do=mycontrol-click&presenter=Test', + $this['mycontrol']->link('click', 0, 1), + ); + Assert::same( + '/index.php?mycontrol-x=0a&mycontrol-y=1a&action=default&do=mycontrol-click&presenter=Test', + $this['mycontrol']->link('click', '0a', '1a'), + ); Assert::same('/index.php?mycontrol-x=1&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', [1])); - Assert::same('#error: Argument $x passed to TestControl::handleClick() must be scalar, array given.', $this['mycontrol']->link('click', [1], (object) [1])); - 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( + '#error: Argument $x passed to TestControl::handleClick() must be scalar, array given.', + $this['mycontrol']->link('click', [1], (object) [1]), + ); + 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#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()); + 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#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(), + ); // Component link type checking - Assert::same("#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, int given.", $this['mycontrol']->link('click', ['order' => 1])); - Assert::same("#error: Value passed to persistent parameter 'round' in component 'mycontrol' must be int, array given.", $this['mycontrol']->link('click', ['round' => []])); + Assert::same( + "#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, int given.", + $this['mycontrol']->link('click', ['order' => 1]), + ); + Assert::same( + "#error: Value passed to persistent parameter 'round' in component 'mycontrol' must be int, array given.", + $this['mycontrol']->link('click', ['round' => []]), + ); $this['mycontrol']->order = 1; - Assert::same("#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, int given.", $this['mycontrol']->link('click')); + Assert::same( + "#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, int given.", + $this['mycontrol']->link('click'), + ); $this['mycontrol']->order = null; // type checking Assert::same('/index.php?action=params&presenter=Test', $this->link('params', [])); - Assert::same(['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same(['int' => null, 'bool' => null, 'str' => null, 'arr' => null, 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?int=1&bool=1&str=abc&arr=1&action=params&presenter=Test', $this->link('params', ['int' => 1, 'bool' => true, 'str' => 'abc', 'arr' => '1'])); - Assert::same('/index.php?int=0&bool=0&action=params&presenter=Test', $this->link('params', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => ''])); + Assert::same( + ['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?action=params&presenter=Test', + $this->link('params', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + [ + 'int' => null, + 'bool' => null, + 'str' => null, + 'arr' => null, + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'action' => 'params', + ], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr=1&action=params&presenter=Test', + $this->link('params', ['int' => 1, 'bool' => true, 'str' => 'abc', 'arr' => '1']), + ); + Assert::same( + '/index.php?int=0&bool=0&action=params&presenter=Test', + $this->link('params', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['int' => new stdClass])); Assert::same('#error: Missing parameter $int required by TestPresenter::actionHints()', $this->link('hints', [])); - Assert::same('#error: Missing parameter $int required by TestPresenter::actionHints()', $this->link('hints', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hints&presenter=Test', $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); - Assert::same('/index.php?int=0&bool=0&action=hints&presenter=Test', $this->link('hints', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHints() must be int, string given.', $this->link('hints', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHints() must be int, stdClass given.', $this->link('hints', ['int' => new stdClass])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHints() must be int, array given.', $this->link('hints', ['int' => []])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionHints() must be bool, string given.', $this->link('hints', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionHints() must be array, string given.', $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + Assert::same( + '#error: Missing parameter $int required by TestPresenter::actionHints()', + $this->link('hints', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hints&presenter=Test', + $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]]), + ); + Assert::same( + '/index.php?int=0&bool=0&action=hints&presenter=Test', + $this->link('hints', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHints() must be int, string given.', + $this->link('hints', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHints() must be int, stdClass given.', + $this->link('hints', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHints() must be int, array given.', + $this->link('hints', ['int' => []]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionHints() must be bool, string given.', + $this->link('hints', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionHints() must be array, string given.', + $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=hintsNulls&presenter=Test', $this->link('hintsNulls', [])); - Assert::same('/index.php?action=hintsNulls&presenter=Test', $this->link('hintsNulls', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNulls&presenter=Test', $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); - Assert::same('/index.php?int=0&bool=0&action=hintsNulls&presenter=Test', $this->link('hintsNulls', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, string given.', $this->link('hintsNulls', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, stdClass given.', $this->link('hintsNulls', ['int' => new stdClass])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, array given.', $this->link('hintsNulls', ['int' => []])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionHintsNulls() must be ?bool, string given.', $this->link('hintsNulls', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionHintsNulls() must be ?array, string given.', $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + Assert::same( + '/index.php?action=hintsNulls&presenter=Test', + $this->link('hintsNulls', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNulls&presenter=Test', + $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]]), + ); + Assert::same( + '/index.php?int=0&bool=0&action=hintsNulls&presenter=Test', + $this->link('hintsNulls', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, string given.', + $this->link('hintsNulls', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, stdClass given.', + $this->link('hintsNulls', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, array given.', + $this->link('hintsNulls', ['int' => []]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionHintsNulls() must be ?bool, string given.', + $this->link('hintsNulls', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionHintsNulls() must be ?array, string given.', + $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=hintsNullable&presenter=Test', $this->link('hintsNullable', [])); - Assert::same('/index.php?action=hintsNullable&presenter=Test', $this->link('hintsNullable', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNullable&presenter=Test', $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); - Assert::same('/index.php?int=0&bool=0&action=hintsNullable&presenter=Test', $this->link('hintsNullable', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, string given.', $this->link('hintsNullable', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, stdClass given.', $this->link('hintsNullable', ['int' => new stdClass])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, array given.', $this->link('hintsNullable', ['int' => []])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionHintsNullable() must be ?bool, string given.', $this->link('hintsNullable', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionHintsNullable() must be ?array, string given.', $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + Assert::same( + '/index.php?action=hintsNullable&presenter=Test', + $this->link('hintsNullable', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNullable&presenter=Test', + $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]]), + ); + Assert::same( + '/index.php?int=0&bool=0&action=hintsNullable&presenter=Test', + $this->link('hintsNullable', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, string given.', + $this->link('hintsNullable', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, stdClass given.', + $this->link('hintsNullable', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, array given.', + $this->link('hintsNullable', ['int' => []]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionHintsNullable() must be ?bool, string given.', + $this->link('hintsNullable', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionHintsNullable() must be ?array, string given.', + $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', [])); - Assert::same('/index.php?action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); - Assert::same(['int' => 1, 'bool' => true, 'str' => 'abc', 'arr' => [1], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'hintsDefaults'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, string given.', $this->link('hintsDefaults', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, stdClass given.', $this->link('hintsDefaults', ['int' => new stdClass])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, array given.', $this->link('hintsDefaults', ['int' => []])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionHintsDefaults() must be bool, string given.', $this->link('hintsDefaults', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionHintsDefaults() must be array, string given.', $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + Assert::same( + '/index.php?action=hintsDefaults&presenter=Test', + $this->link('hintsDefaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsDefaults&presenter=Test', + $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]]), + ); + Assert::same( + [ + 'int' => 1, + 'bool' => true, + 'str' => 'abc', + 'arr' => [1], + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'action' => 'hintsDefaults', + ], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?action=hintsDefaults&presenter=Test', + $this->link('hintsDefaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, string given.', + $this->link('hintsDefaults', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, stdClass given.', + $this->link('hintsDefaults', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, array given.', + $this->link('hintsDefaults', ['int' => []]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionHintsDefaults() must be bool, string given.', + $this->link('hintsDefaults', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionHintsDefaults() must be array, string given.', + $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=defaults&presenter=Test', $this->link('defaults', [])); - Assert::same('/index.php?action=defaults&presenter=Test', $this->link('defaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?action=defaults&presenter=Test', $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => 'a', 'arr' => [1]])); - Assert::same(['int' => null, 'bool' => null, 'str' => null, 'arr' => null, 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'defaults'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?int=0&bool=0&str=&action=defaults&presenter=Test', $this->link('defaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionDefaults() must be int, string given.', $this->link('defaults', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionDefaults() must be int, array given.', $this->link('defaults', ['int' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionDefaults() must be int, stdClass given.', $this->link('defaults', ['int' => new stdClass])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionDefaults() must be bool, string given.', $this->link('defaults', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionDefaults() must be array, string given.', $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); - - Assert::same('/index.php?action=objects&presenter=Test', $this->link('objects', ['req' => new stdClass, 'nullable' => new stdClass, 'opt' => new stdClass])); + Assert::same( + '/index.php?action=defaults&presenter=Test', + $this->link('defaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?action=defaults&presenter=Test', + $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => 'a', 'arr' => [1]]), + ); + Assert::same( + [ + 'int' => null, + 'bool' => null, + 'str' => null, + 'arr' => null, + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'action' => 'defaults', + ], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?int=0&bool=0&str=&action=defaults&presenter=Test', + $this->link('defaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionDefaults() must be int, string given.', + $this->link('defaults', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionDefaults() must be int, array given.', + $this->link('defaults', ['int' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionDefaults() must be int, stdClass given.', + $this->link('defaults', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionDefaults() must be bool, string given.', + $this->link('defaults', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionDefaults() must be array, string given.', + $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); + + Assert::same( + '/index.php?action=objects&presenter=Test', + $this->link('objects', ['req' => new stdClass, 'nullable' => new stdClass, 'opt' => new stdClass]), + ); Assert::same('#error: Missing parameter $req required by TestPresenter::actionObjects()', $this->link('objects', [])); - Assert::same('#error: Missing parameter $req required by TestPresenter::actionObjects()', $this->link('objects', ['req' => null, 'nullable' => null, 'opt' => null])); - Assert::same('#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, Exception given.', $this->link('objects', ['req' => new Exception, 'opt' => null])); - Assert::same('#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, array given.', $this->link('objects', ['req' => []])); + Assert::same( + '#error: Missing parameter $req required by TestPresenter::actionObjects()', + $this->link('objects', ['req' => null, 'nullable' => null, 'opt' => null]), + ); + Assert::same( + '#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, Exception given.', + $this->link('objects', ['req' => new Exception, 'opt' => null]), + ); + Assert::same( + '#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, array given.', + $this->link('objects', ['req' => []]), + ); // silent invalid link mode $this->invalidLinkMode = self::InvalidLinkSilent; @@ -326,5 +632,5 @@ $presenter->injectPrimary( $presenter->invalidLinkMode = TestPresenter::InvalidLinkWarning; $presenter->autoCanonicalize = false; -$request = new Application\Request('Test', Http\Request::Get, []); +$request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); diff --git a/tests/UI/Presenter.paramChecking.phpt b/tests/UI/Presenter.paramChecking.phpt index 3cf341cf4..04f7fe318 100644 --- a/tests/UI/Presenter.paramChecking.phpt +++ b/tests/UI/Presenter.paramChecking.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @require __DIR__ . '/fixtures/ParamPresenter.php'; // @ Required parameter $c follows optional parameter $b diff --git a/tests/UI/Presenter.redirects.phpt b/tests/UI/Presenter.redirects.phpt index 51476ade1..8744d26f7 100644 --- a/tests/UI/Presenter.redirects.phpt +++ b/tests/UI/Presenter.redirects.phpt @@ -8,7 +8,6 @@ use Nette\{Application, Http}; use Nette\Application\Attributes\Persistent; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/functions.php'; diff --git a/tests/UI/Presenter.selfLinkInvalidParam.phpt b/tests/UI/Presenter.selfLinkInvalidParam.phpt index 95bd4e599..193e27a2f 100644 --- a/tests/UI/Presenter.selfLinkInvalidParam.phpt +++ b/tests/UI/Presenter.selfLinkInvalidParam.phpt @@ -9,7 +9,6 @@ use Nette\{Application, Http}; use Nette\Application\UI\{InvalidLinkException, InvalidRequestParameterException, Presenter}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Presenter.storeRequest().phpt b/tests/UI/Presenter.storeRequest().phpt index a409f9ec9..c5bcb7979 100644 --- a/tests/UI/Presenter.storeRequest().phpt +++ b/tests/UI/Presenter.storeRequest().phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -62,7 +61,7 @@ test('request storage with user context', function () { $userMock, ); - $applicationRequest = new Application\Request('', '', []); + $applicationRequest = new Application\Request('', ''); $presenter->run($applicationRequest); $expiration = '+1 year'; @@ -113,7 +112,7 @@ test('request storage without user context', function () { $sessionMock, ); - $applicationRequest = new Application\Request('', '', []); + $applicationRequest = new Application\Request('', ''); $presenter->run($applicationRequest); $expiration = '+1 year'; diff --git a/tests/UI/Requires.forward.link().phpt b/tests/UI/Requires.forward.link().phpt index 7d11fdf69..49700e144 100644 --- a/tests/UI/Requires.forward.link().phpt +++ b/tests/UI/Requires.forward.link().phpt @@ -61,7 +61,7 @@ $presenter->injectPrimary( $presenter->invalidLinkMode = TestPresenter::InvalidLinkWarning; -$request = new Application\Request('Test', Http\Request::Get, []); +$request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); Assert::error( diff --git a/tests/UI/Requires.locations.phpt b/tests/UI/Requires.locations.phpt index 4a907dec1..788a4e265 100644 --- a/tests/UI/Requires.locations.phpt +++ b/tests/UI/Requires.locations.phpt @@ -80,7 +80,6 @@ class TestMethodCreateComponentControl extends Nette\Application\UI\Control } - // class-level attribute $presenter = createPresenter(TestClassPresenter::class); Assert::noError( diff --git a/tests/UI/fixtures/ParamPresenter.php b/tests/UI/fixtures/ParamPresenter.php index 39ad7bc77..235fb8b2d 100644 --- a/tests/UI/fixtures/ParamPresenter.php +++ b/tests/UI/fixtures/ParamPresenter.php @@ -6,7 +6,7 @@ class ParamPresenter extends Nette\Application\UI\Presenter public $bool = true; - public function actionDefault($a, $b = null, array $c, array $d = null, $e = 1, $f = 1.0, $g = false) + public function actionDefault($a, $b = null, array $c, ?array $d = null, $e = 1, $f = 1.0, $g = false) { } } diff --git a/tests/UI/functions.php b/tests/UI/functions.php index 048130af4..d80383b88 100644 --- a/tests/UI/functions.php +++ b/tests/UI/functions.php @@ -1,6 +1,5 @@ createTemplate(); - assertType('Nette\Bridges\ApplicationLatte\Template', $template); + assertType(Nette\Bridges\ApplicationLatte\Template::class, $template); } From be2225870938d83c9ac04d93c10dc52afa9b04d9 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 14 Oct 2026 01:55:13 +0200 Subject: [PATCH 02/54] Request: constants are PascalCase --- src/Application/Application.php | 4 ++-- src/Application/LinkGenerator.php | 2 +- src/Application/Request.php | 15 ++++++++++++--- src/Application/UI/Form.php | 2 +- src/Application/UI/Presenter.php | 6 +++--- tests/Application/Application.run.phpt | 8 ++++---- tests/UI/Presenter.canonicalize.phpt | 2 +- tests/UI/Requires.combinations.phpt | 2 +- tests/UI/Requires.forward.phpt | 2 +- tests/UI/Requires.locations.phpt | 12 ++++++------ 10 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/Application/Application.php b/src/Application/Application.php index d1afc0d4e..3947faf6d 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -131,7 +131,7 @@ public function processRequest(Request $request): Response Arrays::invoke($this->onRequest, $this, $request); if ( - !$request->isMethod($request::FORWARD) + !$request->isMethod($request::Forward) && (!strcasecmp($request->getPresenterName(), (string) $this->errorPresenter) || !strcasecmp($request->getPresenterName(), (string) $this->error4xxPresenter)) ) { @@ -181,7 +181,7 @@ public function createErrorRequest(\Throwable $e): ?Request } } - return new Request($errorPresenter, Request::FORWARD, $args); + return new Request($errorPresenter, Request::Forward, $args); } diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index fccf341c7..12ce0d76f 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -258,7 +258,7 @@ public function createRequest( $args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null; } - return $this->lastRequest = new Request($presenter, Request::FORWARD, $args, flags: ['current' => $current]); + return $this->lastRequest = new Request($presenter, Request::Forward, $args, flags: ['current' => $current]); } diff --git a/src/Application/Request.php b/src/Application/Request.php index 640c5445a..962fe5aba 100644 --- a/src/Application/Request.php +++ b/src/Application/Request.php @@ -25,13 +25,22 @@ final class Request use Nette\SmartObject; /** method */ - public const FORWARD = 'FORWARD'; + public const Forward = 'FORWARD'; /** flag */ - public const RESTORED = 'restored'; + public const Restored = 'restored'; /** flag */ - public const VARYING = 'varying'; + public const Varying = 'varying'; + + /** @deprecated use Request::Forward */ + public const FORWARD = self::Forward; + + /** @deprecated use Request::Restored */ + public const RESTORED = self::Restored; + + /** @deprecated use Request::Varying */ + public const VARYING = self::Varying; public function __construct( diff --git a/src/Application/UI/Form.php b/src/Application/UI/Form.php index b0309f43a..6468bb3e3 100644 --- a/src/Application/UI/Form.php +++ b/src/Application/UI/Form.php @@ -144,7 +144,7 @@ public function signalReceived(string $signal): void } elseif (!$this->crossOrigin && !$presenter->getHttpRequest()->isFrom(Nette\Http\FetchSite::SameOrigin)) { $presenter->detectedCsrf(); - } elseif (!$presenter->getRequest()->hasFlag(Nette\Application\Request::RESTORED)) { + } elseif (!$presenter->getRequest()->hasFlag(Nette\Application\Request::Restored)) { $this->fireEvents(); } } diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index f0160d0e7..9d798b416 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -177,7 +177,7 @@ public function isModuleCurrent(string $module): bool */ public function isForwarded(): bool { - return $this->forwarded || $this->request?->isMethod(Application\Request::FORWARD); + return $this->forwarded || $this->request?->isMethod(Application\Request::Forward); } @@ -819,7 +819,7 @@ public function canonicalize(?string $destination = null, ...$args): void return; } - $code = $request->hasFlag($request::VARYING) + $code = $request->hasFlag($request::Varying) ? Http\IResponse::S302_Found : Http\IResponse::S301_MovedPermanently; $this->sendResponse(new Responses\RedirectResponse($url, $code)); @@ -931,7 +931,7 @@ public function restoreRequest(string $key): void $params[self::FlashKey] = $this->getFlashKey(); $request->setParameters($params); if ($request->isMethod('POST')) { - $request->setFlag(Application\Request::RESTORED); + $request->setFlag(Application\Request::Restored); $this->sendResponse(new Responses\ForwardResponse($request)); } else { $this->redirectUrl($this->linkGenerator->requestToUrl($request)); diff --git a/tests/Application/Application.run.phpt b/tests/Application/Application.run.phpt index 400cacd7a..7a2ef3a9a 100644 --- a/tests/Application/Application.run.phpt +++ b/tests/Application/Application.run.phpt @@ -113,7 +113,7 @@ test('error presenter handling for route mismatch', function () use ($httpReques $requests = $app->getRequests(); Assert::count(1, $requests); - Assert::same(Request::FORWARD, $requests[0]->getMethod()); + Assert::same(Request::Forward, $requests[0]->getMethod()); Assert::same('Error', $requests[0]->getPresenterName()); Assert::equal($requests[0], $errorPresenter->request); @@ -141,7 +141,7 @@ test('error presenter invoked directly via router', function () use ($httpReques Assert::count(2, $requests); Assert::same('GET', $requests[0]->getMethod()); Assert::same('Error', $requests[0]->getPresenterName()); - Assert::same(Request::FORWARD, $requests[1]->getMethod()); + Assert::same(Request::Forward, $requests[1]->getMethod()); Assert::same('Error', $requests[1]->getPresenterName()); Assert::equal($requests[1], $errorPresenter->request); @@ -182,7 +182,7 @@ test('missing presenter fallback to error presenter', function () use ($httpRequ Assert::count(2, $requests); Assert::same('GET', $requests[0]->getMethod()); Assert::same('Missing', $requests[0]->getPresenterName()); - Assert::same(Request::FORWARD, $requests[1]->getMethod()); + Assert::same(Request::Forward, $requests[1]->getMethod()); Assert::same('Error', $requests[1]->getPresenterName()); Assert::equal($requests[1], $errorPresenter->request); @@ -224,7 +224,7 @@ test('presenter exception propagation to error presenter', function () use ($htt Assert::count(2, $requests); Assert::same('GET', $requests[0]->getMethod()); Assert::same('Bad', $requests[0]->getPresenterName()); - Assert::same(Request::FORWARD, $requests[1]->getMethod()); + Assert::same(Request::Forward, $requests[1]->getMethod()); Assert::same('Error', $requests[1]->getPresenterName()); Assert::equal($requests[1], $errorPresenter->request); diff --git a/tests/UI/Presenter.canonicalize.phpt b/tests/UI/Presenter.canonicalize.phpt index 26dd1ef5a..0ce84ea7e 100644 --- a/tests/UI/Presenter.canonicalize.phpt +++ b/tests/UI/Presenter.canonicalize.phpt @@ -172,7 +172,7 @@ test('canonicalize() uses 302 for varying requests', function () { $presenter->autoCanonicalize = true; $request = new Application\Request('Varying', 'GET', ['action' => 'default']); - $request->setFlag($request::VARYING); + $request->setFlag($request::Varying); $response = $presenter->run($request); diff --git a/tests/UI/Requires.combinations.phpt b/tests/UI/Requires.combinations.phpt index 07f60a8ae..942f27414 100644 --- a/tests/UI/Requires.combinations.phpt +++ b/tests/UI/Requires.combinations.phpt @@ -236,7 +236,7 @@ test('forward requirement satisfied', function () { $presenter = createPresenter(ForwardPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('Test', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('Test', Application\Request::Forward)), ); }); diff --git a/tests/UI/Requires.forward.phpt b/tests/UI/Requires.forward.phpt index f275663d2..3ebbce54a 100644 --- a/tests/UI/Requires.forward.phpt +++ b/tests/UI/Requires.forward.phpt @@ -57,7 +57,7 @@ class TestSwitchViewPresenter extends Nette\Application\UI\Presenter // forwarded request $presenter = createPresenter(TestForwardPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward)), ); Assert::exception( diff --git a/tests/UI/Requires.locations.phpt b/tests/UI/Requires.locations.phpt index 788a4e265..6699735ca 100644 --- a/tests/UI/Requires.locations.phpt +++ b/tests/UI/Requires.locations.phpt @@ -83,7 +83,7 @@ class TestMethodCreateComponentControl extends Nette\Application\UI\Control // class-level attribute $presenter = createPresenter(TestClassPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward)), ); Assert::exception( @@ -96,7 +96,7 @@ Assert::exception( // method action() $presenter = createPresenter(TestMethodActionPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward)), ); Assert::exception( @@ -109,7 +109,7 @@ Assert::exception( // method render() $presenter = createPresenter(TestMethodRenderPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward)), ); Assert::exception( @@ -122,7 +122,7 @@ Assert::exception( // method handle() $presenter = createPresenter(TestMethodHandlePresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD, ['do' => 'foo'])), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward, ['do' => 'foo'])), ); Assert::exception( @@ -135,7 +135,7 @@ Assert::exception( // method createComponent() $presenter = createPresenter(TestMethodCreateComponentPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD, ['foo-a' => 1])), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward, ['foo-a' => 1])), ); Assert::exception( @@ -148,7 +148,7 @@ Assert::exception( // method createComponent() in component $presenter = createPresenter(TestMethodCreateComponentPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD, ['bar-foo-a' => 1])), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward, ['bar-foo-a' => 1])), ); Assert::exception( From 51d252a1009b044059a14cb802ef9ca4962da4e5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 14 Oct 2026 02:25:46 +0200 Subject: [PATCH 03/54] uses dresscode --- .gitattributes | 3 ++- .github/workflows/coding-style.yml | 24 ++++++++++----------- dresscode.neon | 33 +++++++++++++++++++++++++++++ src/Application/LinkGenerator.php | 2 +- src/Application/UI/AccessPolicy.php | 4 ++-- 5 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 dresscode.neon diff --git a/.gitattributes b/.gitattributes index 433a2de9a..caba1fad2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,10 +1,11 @@ .gitattributes export-ignore .github/ export-ignore .gitignore export-ignore -ncs.* export-ignore +dresscode.neon export-ignore phpstan*.neon export-ignore src/**/*.latte export-ignore tests/ export-ignore +* text=auto *.php* diff=php *.sh text eol=lf diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 886eb330c..8ac229c34 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -3,29 +3,29 @@ name: Coding Style on: [push, pull_request] jobs: - nette_cc: - name: Nette Code Checker + dresscode: + name: DressCode runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: 8.4 coverage: none - - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress - - run: php temp/code-checker/code-checker --strict-types --no-progress --ignore expected --ignore "src/Bridges/ApplicationTracy/*.latte" + - run: composer create-project dresscode/dresscode temp/dresscode ^1.1 --no-progress + - run: php temp/dresscode/bin/dresscode check - nette_cs: - name: Nette Coding Standard + nette_cc: + name: Nette Code Checker runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: 8.4 coverage: none - - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress - - run: php temp/coding-standard/ecs check + - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress + - run: php temp/code-checker/code-checker --strict-types --no-progress --ignore expected --ignore "src/Bridges/ApplicationTracy/*.latte" diff --git a/dresscode.neon b/dresscode.neon new file mode 100644 index 000000000..10a8a1344 --- /dev/null +++ b/dresscode.neon @@ -0,0 +1,33 @@ +use: + - nette + - modernizations + +typeAnalysis: phpstan + +paths: + - src + - tests + +excludePaths: + - tests/**/expected + - tests/**/fixtures* + +fileExtensions: + - php + - phpt + +imports: {grouping: {class: grouped}} + +overrides: + - + paths: [src] + nameResolution: certain + use: + - compilerOptimizations + - deprecations + + - # the deprecated ArrayAccess keeps its parameter names + paths: + - src/Application/Routers/RouteList.php + - src/Application/UI/Component.php + classes: {overridingMethod: {parameterName: keep}} diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index 12ce0d76f..9d583110f 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -343,7 +343,7 @@ private function validateLinkTarget( if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->isLinkable()) { throw new UI\InvalidLinkException("Link to forbidden $message."); } elseif ($presenter?->invalidLinkMode - && (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class)) + && (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class)) // dresscode:ignore upgrading.declarations.deprecatedMember -- @deprecated is still supported ) { trigger_error("Link to deprecated $message.", E_USER_DEPRECATED); } diff --git a/src/Application/UI/AccessPolicy.php b/src/Application/UI/AccessPolicy.php index 61917f78e..ab516d03f 100644 --- a/src/Application/UI/AccessPolicy.php +++ b/src/Application/UI/AccessPolicy.php @@ -69,7 +69,7 @@ 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') + && !ComponentReflection::parseAnnotation($this->element, 'crossOrigin') // dresscode:ignore upgrading.declarations.deprecatedMember -- @crossOrigin is still supported && !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->sameOrigin === false) ) { $attrs[] = new Attributes\Requires(sameOrigin: true); @@ -125,7 +125,7 @@ private function checkActions(array $actions): void private function checkHttpMethod(array $methods): void { if ($this->element instanceof \ReflectionClass) { - $this->presenter->allowedMethods = []; // bypass Presenter::checkHttpMethod() + $this->presenter->allowedMethods = []; // dresscode:ignore upgrading.declarations.deprecatedMember -- bypass Presenter::checkHttpMethod() } $allowed = array_map(strtoupper(...), $methods); From 95da0aa7b3c8d1ad7d7b77494cb499fb61d08bb5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 14 Oct 2026 02:33:44 +0200 Subject: [PATCH 04/54] added support for PHP 8.6 --- .github/workflows/tests.yml | 2 +- composer.json | 4 ++-- src/Application/LinkGenerator.php | 8 ++++---- tests/Routers/LinkGenerator.phpt | 6 ++++++ tests/UI/Component.isLinkCurrent().asserts.php | 6 ++++++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c2a203de..e62a9bbcc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.3', '8.4', '8.5'] + php: ['8.3', '8.4', '8.5', '8.6'] fail-fast: false diff --git a/composer.json b/composer.json index 08247d89f..a0844179a 100644 --- a/composer.json +++ b/composer.json @@ -11,10 +11,10 @@ } ], "require": { - "php": "8.3 - 8.5", + "php": "8.3 - 8.6", "nette/component-model": "^3.2 || ^4.0", "nette/http": "^3.4", - "nette/routing": "^3.1.1", + "nette/routing": "^3.1.5", "nette/utils": "^4.1" }, "suggest": { diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index 9d583110f..1873a2d07 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -8,7 +8,7 @@ namespace Nette\Application; use Nette\Http\UrlScript; -use Nette\Routing\Router; +use Nette\Routing\{Route, Router}; use Nette\Utils\Reflection; use function array_key_exists, is_scalar, is_string, strlen; @@ -221,8 +221,8 @@ public function createRequest( $globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass); if ($current && $args) { - $tmp = $globalState + $refPresenter->getParameters(); - foreach ($args as $key => $val) { + $tmp = Route::objectsToArrays($globalState + $refPresenter->getParameters()); + foreach (Route::objectsToArrays($args) as $key => $val) { if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) { $current = false; break; @@ -304,7 +304,7 @@ public function requestToUrl(Request $request, ?bool $relative = false): string if ($url === null) { $params = $request->getParameters(); unset($params[UI\Presenter::ActionKey], $params[UI\Presenter::PresenterKey]); - $params = urldecode(http_build_query($params, '', ', ')); + $params = urldecode(http_build_query(Route::objectsToArrays($params), '', ', ')); throw new UI\InvalidLinkException("No route for {$request->getPresenterName()}:{$request->getParameter('action')}($params)"); } diff --git a/tests/Routers/LinkGenerator.phpt b/tests/Routers/LinkGenerator.phpt index 2ae9337f8..8b2de4646 100644 --- a/tests/Routers/LinkGenerator.phpt +++ b/tests/Routers/LinkGenerator.phpt @@ -75,6 +75,12 @@ namespace { }, Nette\Application\UI\InvalidLinkException::class, 'No route for Homepage:default(id=10)'); + testException('route mismatch exception with object parameter', function () use ($pf) { + $generator = new LinkGenerator(new Routers\Route('/', 'Product:'), new Http\UrlScript('http://nette.org/en/'), $pf); + $generator->link('Homepage:default', ['id' => (object) ['a' => 1]]); + }, Nette\Application\UI\InvalidLinkException::class, 'No route for Homepage:default(id[a]=1)'); + + 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]); diff --git a/tests/UI/Component.isLinkCurrent().asserts.php b/tests/UI/Component.isLinkCurrent().asserts.php index 82efda54f..c3548a8ee 100644 --- a/tests/UI/Component.isLinkCurrent().asserts.php +++ b/tests/UI/Component.isLinkCurrent().asserts.php @@ -53,6 +53,12 @@ function callIsComponentLinkCurrent( ['int' => 2], )); +Assert::false(callIsLinkCurrent( + new Application\Request('Test', Http\Request::Get, ['int' => 1, 'bool' => true]), + 'Test:default', + ['foo' => (object) ['a' => 1]], +)); + Assert::false(callIsLinkCurrent( new Application\Request('Test', Http\Request::Get, [Application\UI\Presenter::ActionKey => 'otherAction']), 'Test:default', From 7db59532deb50694087ca7be1e5698d161362746 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 21 Jul 2026 15:21:07 +0200 Subject: [PATCH 05/54] ControlNode: deprecate missing comma before arguments Brings the {form}/{input}/{label} deprecation to {control} via the shared TagParser::consumeCommaBeforeArguments() helper. Triggers E_USER_DEPRECATED when a token follows the name (and optional :method) without a separating comma, pointing to the offending column; the tag still compiles. A trailing |modifier is not mistaken for arguments. --- composer.json | 2 +- .../ApplicationLatte/Nodes/ControlNode.php | 4 +- tests/Bridges.Latte/{control}.2.phpt | 10 ++-- .../Bridges.Latte/{control}.commaWarning.phpt | 53 +++++++++++++++++++ tests/Bridges.Latte/{control}.phpt | 6 +-- 5 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 tests/Bridges.Latte/{control}.commaWarning.phpt diff --git a/composer.json b/composer.json index a0844179a..581e2706a 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "nette/di": "<3.2", "nette/forms": "<3.2", "nette/schema": "<1.3", - "latte/latte": "<3.1.4 || >=3.2", + "latte/latte": "<3.1.5 || >=3.2", "tracy/tracy": "<2.12" }, "autoload": { diff --git a/src/Bridges/ApplicationLatte/Nodes/ControlNode.php b/src/Bridges/ApplicationLatte/Nodes/ControlNode.php index 5bb6f30a5..43f9d3926 100644 --- a/src/Bridges/ApplicationLatte/Nodes/ControlNode.php +++ b/src/Bridges/ApplicationLatte/Nodes/ControlNode.php @@ -17,7 +17,7 @@ /** - * {control name[:method] [,] [params]} + * {control name[:method] [, params]} * Renders a component by calling its render() method. */ class ControlNode extends StatementNode @@ -39,7 +39,7 @@ public static function create(Tag $tag): static $node->method = $tag->parser->parseExpression(); } - $stream->tryConsume(','); + $tag->parser->consumeCommaBeforeArguments(strict: false); $start = $stream->getIndex(); $node->args = $tag->parser->parseArguments(); $start -= $stream->getIndex(); diff --git a/tests/Bridges.Latte/{control}.2.phpt b/tests/Bridges.Latte/{control}.2.phpt index e9bda2a50..e7e85f31c 100644 --- a/tests/Bridges.Latte/{control}.2.phpt +++ b/tests/Bridges.Latte/{control}.2.phpt @@ -51,15 +51,15 @@ $latte->renderToString(' {control $name:$name} -{control form var1} +{control form, var1} -{control form var1, 1, 2} +{control form, var1, 1, 2} -{control form wrap => 5, 1, 2} +{control form, wrap => 5, 1, 2} -{control form [var1 => 5], 1, 2} +{control form, [var1 => 5], 1, 2} -{control form 5, 1, var1: 2} +{control form, 5, 1, var1: 2} ', $params); Assert::same([ diff --git a/tests/Bridges.Latte/{control}.commaWarning.phpt b/tests/Bridges.Latte/{control}.commaWarning.phpt new file mode 100644 index 000000000..1a5dd3281 --- /dev/null +++ b/tests/Bridges.Latte/{control}.commaWarning.phpt @@ -0,0 +1,53 @@ +setLoader(new Latte\Loaders\StringLoader); +$latte->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension(null)); + + +test('no warning when only the component name is given', function () use ($latte) { + Assert::noError(fn() => $latte->compile('{control form}')); +}); + + +test('no warning when comma separates name and arguments', function () use ($latte) { + Assert::noError(fn() => $latte->compile('{control form, param => 1}')); +}); + + +test('deprecation warning when comma is missing before arguments', function () use ($latte) { + Assert::error( + fn() => $latte->compile('{control form param => 1}'), + E_USER_DEPRECATED, + 'Missing comma before tag arguments on line 1 at column 15.', + ); +}); + + +test('deprecation warning also fires after the :method syntax', function () use ($latte) { + Assert::error( + fn() => $latte->compile('{control form:type param}'), + E_USER_DEPRECATED, + 'Missing comma before tag arguments on line 1 at column 20.', + ); +}); + + +test('a malformed tag emits the deprecation together with the exception', function () use ($latte) { + Assert::error(function () use ($latte) { + Assert::exception( + fn() => $latte->compile('{control form: param => 1}'), + Latte\CompileException::class, + "Unexpected '=>' (on line 1 at column 22)", + ); + }, E_USER_DEPRECATED, 'Missing comma before tag arguments on line 1 at column 22.'); +}); diff --git a/tests/Bridges.Latte/{control}.phpt b/tests/Bridges.Latte/{control}.phpt index a3c59f235..dccddaaba 100644 --- a/tests/Bridges.Latte/{control}.phpt +++ b/tests/Bridges.Latte/{control}.phpt @@ -60,7 +60,7 @@ Assert::match( $ʟ_tmp->renderType('param') /* pos 1:1 */; %A% XX, - $latte->compile('{control form:type param}'), + $latte->compile('{control form:type, param}'), ); Assert::match( @@ -69,7 +69,7 @@ Assert::match( $ʟ_tmp->render(...$params) /* pos 1:1 */; %A% XX, - $latte->compile('{control form (expand) $params}'), + $latte->compile('{control form, (expand) $params}'), ); Assert::match( @@ -78,7 +78,7 @@ Assert::match( $ʟ_tmp->renderType(['param' => 123]) /* pos 1:1 */; %A% XX, - $latte->compile('{control form:type param => 123}'), + $latte->compile('{control form:type, param => 123}'), ); Assert::match( From 9d6183f883b266314bb7a510cb98b9961b9e7654 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 27 Jul 2026 23:14:38 +0200 Subject: [PATCH 06/54] added support for backed enums in parameters [Closes nette/nette#1558] Persistent, action, render and signal parameters can be typed as a backed enum. The URL carries the case value: link() unwraps BackedEnum instances before the URL is built (http_build_query would otherwise decompose the object into an array), and incoming scalars are losslessly converted back via tryFrom(); an invalid value yields 4xx, not 500. --- src/Application/LinkGenerator.php | 13 ++- src/Application/UI/ParameterConverter.php | 26 ++++- tests/UI/ParameterConverter.convertType.phpt | 37 ++++++++ tests/UI/Presenter.link().enum.phpt | 99 ++++++++++++++++++++ 4 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 tests/UI/Presenter.link().enum.phpt diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index 1873a2d07..9b19fd372 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -221,9 +221,10 @@ public function createRequest( $globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass); if ($current && $args) { - $tmp = Route::objectsToArrays($globalState + $refPresenter->getParameters()); - foreach (Route::objectsToArrays($args) as $key => $val) { - if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) { + $tmp = $globalState + $refPresenter->getParameters(); + $serialize = fn($val) => http_build_query(Route::objectsToArrays([$val instanceof \BackedEnum ? $val->value : $val])); + foreach ($args as $key => $val) { + if ($serialize($val) !== (isset($tmp[$key]) ? $serialize($tmp[$key]) : '')) { $current = false; break; } @@ -258,6 +259,12 @@ public function createRequest( $args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null; } + foreach ($args as $key => $val) { + if ($val instanceof \BackedEnum) { // URL carries the case value + $args[$key] = $val->value; + } + } + return $this->lastRequest = new Request($presenter, Request::Forward, $args, flags: ['current' => $current]); } diff --git a/src/Application/UI/ParameterConverter.php b/src/Application/UI/ParameterConverter.php index cf6f3020e..e83d56d4b 100644 --- a/src/Application/UI/ParameterConverter.php +++ b/src/Application/UI/ParameterConverter.php @@ -153,7 +153,8 @@ public static function convertType(mixed &$val, string $types): bool $type === 'scalar' => !is_array($val), // special type due to historical reasons $type === 'mixed' => true, $type === 'callable' => false, // intentionally disabled for security reasons - default => $val instanceof $type, + default => $val instanceof $type + || (is_subclass_of($type, \BackedEnum::class) && self::castEnum($val, $type)), }) { return true; } @@ -189,6 +190,29 @@ private static function castScalar(mixed &$val, string $type): bool } + /** + * Lossless conversion of scalar to backed enum case. + * @param class-string<\BackedEnum> $type + */ + private static function castEnum(mixed &$val, string $type): bool + { + if (!is_scalar($val)) { + return false; + } + + $tmp = $val; + if ( + !self::castScalar($tmp, (string) (new \ReflectionEnum($type))->getBackingType()) + || !($case = $type::tryFrom($tmp)) + ) { + return false; + } + + $val = $case; + return true; + } + + public static function getType(\ReflectionParameter|\ReflectionProperty $item): string { if ($type = $item->getType()) { diff --git a/tests/UI/ParameterConverter.convertType.phpt b/tests/UI/ParameterConverter.convertType.phpt index e3406f026..573b61aea 100644 --- a/tests/UI/ParameterConverter.convertType.phpt +++ b/tests/UI/ParameterConverter.convertType.phpt @@ -32,6 +32,18 @@ function testIt(string $type, $val, $res = null) } +enum TestStringEnum: string +{ + case Foo = 'foo'; + case One = '1'; +} + +enum TestIntEnum: int +{ + case One = 1; +} + + $obj = new stdClass; testIt('scalar', null, null); @@ -272,3 +284,28 @@ testIt('int|array', 0, 0); testIt('int|array', 1, 1); testIt('int|array', 1.0, 1); testIt('int|array', 1.2); + + +testIt(TestStringEnum::class, null); +testIt(TestStringEnum::class, []); +testIt(TestStringEnum::class, $obj); +testIt(TestStringEnum::class, TestStringEnum::Foo, TestStringEnum::Foo); +testIt(TestStringEnum::class, TestIntEnum::One); +testIt(TestStringEnum::class, 'foo', TestStringEnum::Foo); +testIt(TestStringEnum::class, 'bar'); +testIt(TestStringEnum::class, ''); +testIt(TestStringEnum::class, 1, TestStringEnum::One); +testIt(TestStringEnum::class, true, TestStringEnum::One); + +testIt(TestIntEnum::class, null); +testIt(TestIntEnum::class, TestIntEnum::One, TestIntEnum::One); +testIt(TestIntEnum::class, '1', TestIntEnum::One); +testIt(TestIntEnum::class, 1, TestIntEnum::One); +testIt(TestIntEnum::class, 1.0, TestIntEnum::One); +testIt(TestIntEnum::class, '1.0'); +testIt(TestIntEnum::class, 2); +testIt(TestIntEnum::class, 'foo'); + +testIt('?' . TestStringEnum::class, 'foo', TestStringEnum::Foo); +testIt(TestStringEnum::class . '|int', 2, 2); +testIt(TestStringEnum::class . '|int', 'foo', TestStringEnum::Foo); diff --git a/tests/UI/Presenter.link().enum.phpt b/tests/UI/Presenter.link().enum.phpt new file mode 100644 index 000000000..84bf72514 --- /dev/null +++ b/tests/UI/Presenter.link().enum.phpt @@ -0,0 +1,99 @@ +sort); + + // non-default value is carried in URL as the case value + Assert::same('/index.php?sort=desc&action=default&presenter=Test', $this->link('this')); + + // enum instance and its value are accepted in args + Assert::same('/index.php?sort=desc&action=default&presenter=Test', $this->link('this', ['sort' => Sort::Desc])); + Assert::same('/index.php?sort=desc&action=default&presenter=Test', $this->link('this', ['sort' => 'desc'])); + + // default value is omitted + Assert::same('/index.php?action=default&presenter=Test', $this->link('this', ['sort' => Sort::Asc])); + $this->sort = Sort::Asc; + Assert::same('/index.php?action=default&presenter=Test', $this->link('this')); + + // enum in action method argument + Assert::same('/index.php?dir=desc&action=other&presenter=Test', $this->link('other', ['dir' => Sort::Desc])); + Assert::same('/index.php?dir=desc&action=other&presenter=Test', $this->link('other', ['dir' => 'desc'])); + Assert::same('/index.php?action=other&presenter=Test', $this->link('other', ['dir' => Sort::Asc])); + + $this->terminate(); + } + + + public function actionOther(Sort $dir = Sort::Asc): void + { + } +} + + +function createPresenter(): TestPresenter +{ + $url = new Http\UrlScript('http://localhost/index.php', '/index.php'); + $presenterFactory = Mockery::mock(Nette\Application\IPresenterFactory::class); + $presenterFactory->shouldReceive('getPresenterClass') + ->andReturnUsing(fn($presenter) => $presenter . 'Presenter'); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request($url), + new Http\Response, + $presenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + return $presenter; +} + + +createPresenter()->run(new Application\Request('Test', Http\Request::Get, ['sort' => 'desc'])); + + +Assert::exception( + fn() => createPresenter()->run(new Application\Request('Test', Http\Request::Get, ['sort' => 'bogus'])), + Application\BadRequestException::class, + "Value passed to persistent parameter 'sort' in presenter Test must be Sort, string given.", +); + + +Assert::exception( + fn() => createPresenter()->run(new Application\Request('Test', Http\Request::Get, ['sort' => ['name' => 'desc', 'value' => 'desc']])), + Application\BadRequestException::class, + "Value passed to persistent parameter 'sort' in presenter Test must be Sort, array given.", +); + + +Assert::exception( + fn() => createPresenter()->run(new Application\Request('Test', Http\Request::Get, ['action' => 'other', 'dir' => 'bogus'])), + Application\BadRequestException::class, + 'Argument $dir passed to TestPresenter::actionOther() must be Sort, string given.', +); From a2a6c6acb3adc99cd19b250eac03500e54c203f3 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 5 Aug 2026 14:36:11 +0200 Subject: [PATCH 07/54] removed withoutUserInfo() calls, nette/http no longer passes user info into the request URL --- src/Bridges/ApplicationDI/ApplicationExtension.php | 2 +- src/Bridges/ApplicationLatte/TemplateFactory.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php index c95f478ba..90f808b72 100644 --- a/src/Bridges/ApplicationDI/ApplicationExtension.php +++ b/src/Bridges/ApplicationDI/ApplicationExtension.php @@ -131,7 +131,7 @@ public function loadConfiguration(): void $builder->addDefinition($this->prefix('linkGenerator')) ->setCreator(Nette\Application\LinkGenerator::class, [ - 1 => new Definitions\Statement([new Definitions\Statement('@Nette\Http\IRequest::getUrl'), 'withoutUserInfo']), + 1 => new Definitions\Statement('@Nette\Http\IRequest::getUrl'), ]); if ($this->name === 'application') { diff --git a/src/Bridges/ApplicationLatte/TemplateFactory.php b/src/Bridges/ApplicationLatte/TemplateFactory.php index fbc112219..bfc843fd6 100644 --- a/src/Bridges/ApplicationLatte/TemplateFactory.php +++ b/src/Bridges/ApplicationLatte/TemplateFactory.php @@ -62,7 +62,7 @@ private function injectDefaultVariables(Template $template, ?UI\Control $control { $presenter = $control?->getPresenter(throw: false); $baseUrl = $this->httpRequest - ? rtrim($this->httpRequest->getUrl()->withoutUserInfo()->getBaseUrl(), '/') + ? rtrim($this->httpRequest->getUrl()->getBaseUrl(), '/') : null; $flashes = $presenter instanceof UI\Presenter && $presenter->hasFlashSession() ? (array) $presenter->getFlashSession()->get($control->getParameterId('flash')) From 0bd21335d0c81a38cc70521f4701ccd2e3d2dcd0 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 5 Aug 2026 16:58:47 +0200 Subject: [PATCH 08/54] Application: initial request with HTTP method FORWARD is rejected, it allowed reaching the error presenter directly (thanks to Jakub Richtr) --- src/Application/Application.php | 4 ++++ tests/Application/Application.run.phpt | 27 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/Application/Application.php b/src/Application/Application.php index 3947faf6d..7a70369dd 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -95,6 +95,10 @@ public function run(): void */ public function createInitialRequest(): Request { + if ($this->httpRequest->isMethod(Request::Forward)) { + throw new BadRequestException('Invalid request. Method FORWARD is reserved for internal use.'); + } + $params = $this->router->match($this->httpRequest); $presenter = $params[UI\Presenter::PresenterKey] ?? null; diff --git a/tests/Application/Application.run.phpt b/tests/Application/Application.run.phpt index 7a2ef3a9a..49d465d14 100644 --- a/tests/Application/Application.run.phpt +++ b/tests/Application/Application.run.phpt @@ -77,6 +77,7 @@ class UIErrorPresenter extends Nette\Application\UI\Presenter $httpRequest = Mockery::mock(Nette\Http\IRequest::class); $httpRequest->shouldReceive('getMethod')->andReturn('GET'); +$httpRequest->shouldReceive('isMethod')->with(Request::Forward)->andReturn(false); $httpRequest->shouldReceive('getPost')->andReturn([]); $httpRequest->shouldReceive('getFiles')->andReturn([]); $httpRequest->shouldReceive('isSecured')->andReturn(false); @@ -151,6 +152,32 @@ test('error presenter invoked directly via router', function () use ($httpReques }); +test('error presenter is not reachable by HTTP method FORWARD', function () use ($httpResponse) { + foreach (['FORWARD', 'forward'] as $method) { + $httpRequest = new Nette\Http\Request(new Nette\Http\UrlScript, method: $method); + $errorPresenter = new ErrorPresenter; + + $presenterFactory = Mockery::mock(IPresenterFactory::class); + $presenterFactory->shouldReceive('createPresenter')->with('Error')->andReturn($errorPresenter); + + $router = Mockery::mock(Router::class); + $router->shouldReceive('match')->andReturn(['presenter' => 'Error']); + + $app = new Application($presenterFactory, $router, $httpRequest, $httpResponse); + $app->errorPresenter = 'Error'; + $app->run(); + + $requests = $app->getRequests(); + Assert::count(1, $requests); + Assert::same(Request::Forward, $requests[0]->getMethod()); + Assert::null($requests[0]->getParameter('request')); + $e = $errorPresenter->request->getParameter('exception'); + Assert::type(BadRequestException::class, $e); + Assert::same('Invalid request. Method FORWARD is reserved for internal use.', $e->getMessage()); + } +}); + + testException('missing presenter exception without error presenter', function () use ($httpRequest, $httpResponse) { $presenterFactory = Mockery::mock(IPresenterFactory::class); $presenterFactory->shouldReceive('createPresenter')->with('Missing')->andThrow(Nette\Application\InvalidPresenterException::class); From 627ec3e86d8d523eac71aa0f3a523c9e983f2df9 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 13 Aug 2026 22:47:05 +0200 Subject: [PATCH 09/54] added AGENTS.md & DOCS --- .gitattributes | 2 + AGENTS.md | 61 ++++++++++++++++ docs/decisions/2025-02-27 linkbase-tag.md | 81 ++++++++++++++++++++++ docs/internals/access-control.md | 58 ++++++++++++++++ docs/internals/lifecycle.md | 75 ++++++++++++++++++++ docs/internals/link-generation.md | 84 +++++++++++++++++++++++ docs/internals/persistent-state.md | 58 ++++++++++++++++ docs/internals/readme.md | 21 ++++++ docs/internals/routing.md | 60 ++++++++++++++++ docs/internals/snippets.md | 46 +++++++++++++ docs/internals/templates.md | 56 +++++++++++++++ 11 files changed, 602 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/decisions/2025-02-27 linkbase-tag.md create mode 100644 docs/internals/access-control.md create mode 100644 docs/internals/lifecycle.md create mode 100644 docs/internals/link-generation.md create mode 100644 docs/internals/persistent-state.md create mode 100644 docs/internals/readme.md create mode 100644 docs/internals/routing.md create mode 100644 docs/internals/snippets.md create mode 100644 docs/internals/templates.md diff --git a/.gitattributes b/.gitattributes index caba1fad2..7ff79987f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,11 @@ .gitattributes export-ignore .github/ export-ignore .gitignore export-ignore +AGENTS.md export-ignore dresscode.neon export-ignore phpstan*.neon export-ignore src/**/*.latte export-ignore +docs/ export-ignore tests/ export-ignore * text=auto diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..40665db71 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# To My Agents! + +It is my fervent wish that this file guide every AI coding agent working with code in this repository. + +## Documentation + +Any distilled, agent-facing documentation for this package - how it works +internally and the rationale behind key design decisions - lives in `docs/`. +Consult it before non-trivial changes; it is the source of truth from which the +public manual is distilled. + +The presenter lifecycle, link generation, persistent state, routing, and template +lookup carry subtle non-local invariants - read `docs/internals` before touching +them. + +## Project Overview + +Nette Application is a full-stack, component-based MVC framework library for PHP - +the core application layer of the Nette Framework: presenters, components, routing, +templating integration, and the request/response cycle. It is a library package, +not a runnable application, and integrates with the rest of the framework through +`src/Bridges` (DI, Latte, Tracy). + +- **PHP Version**: 8.3 - 8.5 +- **Package**: `nette/application` + +## Essential Commands + +```bash +# Run all tests +composer tester # or: vendor/bin/tester tests -s + +# Run one test directory / file +vendor/bin/tester tests/UI -s +php tests/Application/Presenter.twoDomains.phpt + +# Static analysis (PHPStan level 8) +composer phpstan +``` + +## Conventions + +- Every file starts with `declare(strict_types=1);`; tabs for indentation; Nette + Coding Standard (PSR-12 based) with the opening brace on its own line. +- Tests are Nette Tester `.phpt` files mirroring `src/` under `tests/`; use + `test()` / `testException()` and `Assert::*`. Temp files go to `tests/tmp/{pid}/`, + and `tests/types/` holds PHPStan type-inference fixtures. + +## Working in this repo + +- **The presenter lifecycle is exact and load-bearing.** The precise phase order + (action, canonicalization, signal dispatch, `beforeRender`, `render`, the + `onRender` hook, `afterRender`), `AbortException`-based termination, and the + 4xx-not-500 invalid-link contract are documented in `docs/internals/lifecycle.md`. + Don't reconstruct them from memory. +- **Links and persistent state span the whole component tree.** Link-generation + modes and whole-tree global-state aggregation are the trickiest code here; see + `docs/internals/link-generation.md` and `persistent-state.md`. +- User-facing how-to (routing DSL, signals, snippets, component factories, flash + messages, DI/NEON configuration) is manual material and lives in the public web + docs, not here. diff --git a/docs/decisions/2025-02-27 linkbase-tag.md b/docs/decisions/2025-02-27 linkbase-tag.md new file mode 100644 index 000000000..8ebba910f --- /dev/null +++ b/docs/decisions/2025-02-27 linkbase-tag.md @@ -0,0 +1,81 @@ +# {linkBase} tag - compile-time base for relative links in a template + +Status: accepted · 2025-02-27 · branch `v3.2` (released in 3.2.7) + +## Context + +A flexible directory structure is one of the most valuable features of Nette +Application, but it creates a problem with shared layouts: relative link targets +(`Dashboard:`, `Products:Overview:`) are resolved against the *current* +presenter. When the same layout is used by both `Admin:Dashboard` and +`Admin:Products:Detail`, the target `Dashboard:` resolves correctly to +`Admin:Dashboard:default` in one case and to the non-existent +`Admin:Products:Dashboard:default` in the other. + +The previous workaround was to write every target in layouts absolutely +(`:Admin:Dashboard:`). It works, but the module prefix is duplicated in every +link, so moving or renaming a module means editing templates en masse. + +## Considered options + +- **Status quo (absolute links everywhere)** - no new concept, but prefix + duplication and fragility when refactoring modules. +- **Runtime solution** (setting the base on the presenter/component, or a + template variable threaded into `LinkGenerator`) - collides with the shared + template cache: the same compiled layout is used by presenters from different + modules, so the base would have to become part of the cache key, or a runtime + cost would be paid on every link. It would also make the template no longer + self-describing - a link's target would depend on state set somewhere in PHP. +- **Compile-time tag in the template** (chosen) - the base is declared directly + in the template it concerns, and expansion happens at compile time. + +## Decision + +A new tag `{linkBase module}` plus a compile-time pass `applyLinkBasePass` +registered in `UIExtension::getPasses()`: + +- The tag must appear in the template head (`Tag::isInHead()`, otherwise + `CompileException`) and applies to the **whole template file** - not to + included files, which are compiled separately and may carry their own + `{linkBase}`. It emits no code itself (`print()` returns `''`); all the work + is done by the pass. Only the first tag found is used + (`NodeHelpers::findFirst`). +- The pass traverses the AST and rewrites **all** `LinkNode`s - i.e. `{link}`, + `{plink}` and `n:href` uniformly. +- When both the target and the base are static strings, the destination is + rewritten directly at compile time (`LinkGenerator::applyBase()` called from + the pass) - **zero runtime overhead**. When either is an expression, the + destination is wrapped in a runtime call `LinkGenerator::applyBase(%node, + %node)` via `AuxiliaryNode`. +- Semantics of `LinkGenerator::applyBase($link, $base)`: the prefix `:$base:` is + added only when the target **contains a colon and does not start with one**. + Thus the base applies exclusively to relative targets that name a presenter + (`Users:list` → `:Admin:Users:list`); untouched are absolute targets + (`:Foo:`), targets within the current presenter (`show`, `this`), signals + (`click!`) and aliases - none of which have a colon in the middle. + +## Consequences + +- (+) Layouts and templates shared by presenters at different module depths link + consistently, regardless of who renders them. +- (+) Renaming or moving a module is a single change in `{linkBase}` instead of + editing every link. +- (+) For static links, expansion happens at compile time; nothing is paid at + runtime. +- (−) Another tag and concept to learn; the per-template-file scope may surprise + (includes do not inherit the base). +- (−) The base cannot be set per presenter/module from PHP - that remains open. + +## Code + +- `src/Bridges/ApplicationLatte/Nodes/LinkBaseNode.php` - the tag and the + `applyLinkBasePass` pass +- `src/Application/LinkGenerator.php` - `applyBase()` (`@internal`) +- tag and pass registration in `src/Bridges/ApplicationLatte/UIExtension.php` + (`getTags()`, `getPasses()`) +- test: `tests/Bridges.Latte/{linkBase}.phpt` (static and dynamic targets) +- commit `e3beab847`, released in `v3.2.7` + +The `|absoluteUrl` filter shipped in the same release for forcing an absolute URL +on an arbitrary value (image path, database value), complementing this tag for +cases outside presenter link targets. diff --git a/docs/internals/access-control.md b/docs/internals/access-control.md new file mode 100644 index 000000000..47235eb6f --- /dev/null +++ b/docs/internals/access-control.md @@ -0,0 +1,58 @@ +# Access control & CSRF + +`AccessPolicy` (internal) evaluates `#[Requires]` attributes. The attribute is +repeatable, class- or method-level, and matched with `IS_INSTANCEOF`, so subclasses +(like the deprecated `#[CrossOrigin]`) count as `Requires` instances. + +## Enforcement runs at three sites + +No single file shows this; the policy is applied wherever user code is about to run: + +1. **Class-level** — `Presenter::run()`, in the checkRequirements phase, on the + presenter's own reflection (before `startup`). +2. **Method-level** — `Component::tryCall()`, i.e. on every `action*`, `render*` + and `handle*` invocation, right before argument conversion. +3. **Factory methods** — `Component::createComponent()` checks the policy on the + `createComponent()` method, so a component can be guarded at creation. + +At the first two sites the (older, unrelated) `checkRequirements()` user hook is +also called with the same reflection — the two mechanisms coexist. The factory +site runs only the attribute policy. + +## `#[Requires]` semantics + +- `methods:` — allowed HTTP methods; `['*']` is a wildcard. **Trap:** a + *class-level* `methods:` empties `$presenter->allowedMethods` to bypass the + deprecated legacy `Presenter::checkHttpMethod()`, so the attribute fully replaces + it. Violation → 405 with an `Allow` header. +- `actions:` — method-level use is allowed **only in presenters** + (`LogicException` otherwise). Violation → 4xx via `error()`. +- `forward: true` — the element is reachable only via forward; it also makes the + element **non-linkable**: `AccessPolicy::isLinkable()` is what + `LinkGenerator::validateLinkTarget` consults for every mode except `forward` + (see link-generation.md). Violation at runtime → 4xx. +- `ajax: true` — non-AJAX request → 403. +- `sameOrigin: true` — see below. + +## The implicit signal CSRF rule + +**Every `handle*` method implicitly gets `Requires(sameOrigin: true)`** — added by +`AccessPolicy::applyInternalRules` — unless the method opts out via +`#[Requires(sameOrigin: false)]`, the deprecated `#[CrossOrigin]`, or a legacy +`@crossOrigin` annotation. Same-origin is verified from the **`Sec-Fetch-Site` +header** (`$httpRequest->isFrom(FetchSite::SameOrigin)`), not from a SameSite +cookie. + +A violation does **not** produce an error: it calls `Presenter::detectedCsrf()`, +whose default is `redirect('this')` — **the signal is silently dropped** and the +page reloads without it. `detectedCsrf()` is public and overridable. + +## `UI\Form` has its own, parallel check + +`Form::signalReceived('submit')` does **not** go through `AccessPolicy` for CSRF: +it re-checks `Sec-Fetch-Site` itself, gated by the `$crossOrigin` flag from +nette/forms (`allowCrossOrigin()`), and calls the same `detectedCsrf()`. Two more +suppressions live here: a request with the `RESTORED` flag (set by +`Presenter::restoreRequest` for re-sent POSTs) skips `fireEvents()` entirely, and +`receiveHttpData()` returns null for forwarded requests — a form never submits +across a forward. diff --git a/docs/internals/lifecycle.md b/docs/internals/lifecycle.md new file mode 100644 index 000000000..33f487ec9 --- /dev/null +++ b/docs/internals/lifecycle.md @@ -0,0 +1,75 @@ +# Presenter lifecycle + +`Presenter::run(Request): Response` is a fixed sequence; the exact ordering is the +non-local knowledge (some steps sit where a reader would not expect). + +## Phase order (`run`) + +Before the `try`: `initGlobalParameters()` parses the action, the signal, and the +signal receiver, and calls `loadState()`. Then, inside the `try`: + +1. **checkRequirements** — access policy, `checkRequirements`, HTTP method check. +2. **startup** — `onStartup`, then `startup()`, then a guard that the child called + `parent::startup()` (else `InvalidStateException`). +3. **`action()`** — via `tryCall`. A `SwitchException` here re-enters with a + changed action **and disables `autoCanonicalize`**. +4. **component autoload** — every id in `globalParams` is `getComponent(…, throw: + false)`d. +5. **canonicalize** — only if `autoCanonicalize` (see below). +6. **HEAD** — terminates early on a HEAD request. +7. **`handle()`** — `processSignal()`. **This runs after the action and + before rendering**, which is the ordering that surprises people. +8. **beforeRender** → **`onRender`** (a hook that is easy to forget) → **`render()`** + (again `SwitchException` → `setView` + retry) → **afterRender**. +9. **sendTemplate** — completes and sends the template response. + +So the true order is **startup → action → (autoload + canonicalize) → signal → +beforeRender → onRender → render → afterRender → sendTemplate**. + +A `SwitchException` escaping anywhere other than the two retry sites above is +turned by `run()` into a `LogicException` ("Switch is only allowed inside +action*() or render*() method"). + +## Termination is by exception; there is no explicit "frozen" flag + +Every terminating method funnels through `sendResponse()` → `terminate()`, which +**throws `AbortException`**: `sendTemplate`, `sendPayload`/`sendJson`, +`redirectUrl`, `forward`. `run()`'s **empty** `catch (AbortException)` then lets +control fall through to the post-lifecycle steps. So there is no separate "state is +frozen" mechanism — the response method simply throws, and nothing after it in the +lifecycle runs. (`error()` is different: it throws `BadRequestException`, which is +**not** caught in `run()` and bubbles up to `Application`.) + +**Post-lifecycle, always runs** (even after redirect/JSON, because the abort lands +here): `saveGlobalState()`, then the AJAX payload block (`payload->state = +getGlobalState()`, snippet mode for an invalidated `TextResponse`), flash-session +30-second expiration, a `VoidResponse` fallback, `onShutdown`, `shutdown()`, and +`return $this->response`. + +## The template file is resolved lazily, at `sendTemplate` + +`completeTemplate()` copies the presenter's `#[TemplateVariable]` properties into +the template (see templates.md) and, **only if the template has no file set**, +calls `findTemplateFile()` **then** — so template lookup happens at send time, not +when the template object is created. Changing the view is possible right +up to `render`; after `sendTemplate` throws `AbortException` nothing more can +affect it. + +## Canonicalization uses the `redirectX` mode + +`canonicalize()` (auto-called at step 5 unless `autoCanonicalize` was turned off by +an action switch) returns immediately for AJAX or non-GET/HEAD. Otherwise it builds +the canonical URL with `link(..., 'redirectX')` (see link-generation.md — an +absolute URL with no flash key, used purely for comparison), swallows any +`InvalidLinkException`, and if the current URL differs, redirects — **302 when the +`VARYING` request flag is set, else 301**. That flag is set by the router (external +`nette/routing`); this package only reads it. + +## Signals + +`initGlobalParameters` reads the signal from `_do` (POST) / `do` (the `SignalKey`), +splitting `{receiver}-{signal}` at the **last** `-` (so a receiver id may itself +contain dashes). `processSignal()` resolves the receiver (`''` = the presenter +itself), raising `BadSignalException` if it is missing or not a `SignalReceiver`, +then calls `signalReceived()` → `handle()`. A signal always targets the +current presenter/action; the URL carries it as `?do=-`. diff --git a/docs/internals/link-generation.md b/docs/internals/link-generation.md new file mode 100644 index 000000000..02d4a61b2 --- /dev/null +++ b/docs/internals/link-generation.md @@ -0,0 +1,84 @@ +# Link generation + +`LinkGenerator::createRequest` is the densest, most trap-laden code in +the package. It builds a `Request` from a destination string, in five phases: +(1) signal/component resolution, (2) alias + presenter/action split, (3) signal +argument processing, (4) action argument processing, (5) missing-param check and +assembly. + +## The mode strings — and `redirectX` is real + +The `$mode` string is **not** just `link`/`redirect`/`forward`. There are **five**: + +| mode | URL from `link()`? | flash `_fid`? | notes | +|---|---|---|---| +| `link` | relative (unless `absoluteUrls` or `//` prefix) | no | the normal case | +| `redirect` | absolute | yes* | `Component::redirect` | +| `forward` | — (returns null) | yes* | Request via `$lastRequest`; no `isLinkable` enforcement | +| `test` | — (returns null) | no | `isLinkCurrent`; skips the missing-param check | +| `redirectX` | absolute | no | **canonicalization only** | + +`redirectX` is **not a phantom** — it is live in `Presenter::canonicalize()`. It +behaves like a redirect that yields an **absolute URL with no flash key**, so the +result can be compared byte-for-byte against the current request URL. The +`@param` docblock under-documents it (it lists only `forward|redirect|link`); trust +the code. `*`Flash is added only when the mode is `redirect`/`forward` **and** the +presenter has an active flash session — never for `redirectX` or `link`. + +## The `'#'` argument is reserved for the fragment + +`link()` (not `createRequest`) strips a `'#'` key out of `$args` and appends it as +the URL fragment — rawurlencoded, overriding any `#fragment` in the destination +string (an empty-string value falls back to it); a non-scalar value throws +`InvalidLinkException`. So `'#'` never reaches the `Request`, and no real parameter +may be named `#`. A `?query` inside the destination is deprecated and **replaces** +`$args` entirely (`parseDestination` returns it as `args`). + +## `$lastRequest` is an out-of-band return channel + +`createRequest` returns a URL, but also stores the built `Request` in the public +`@internal $lastRequest` (reset at the start, set at the end). This is how callers +retrieve the request without it being the return value — `getLastCreatedRequest()`, +the `'current'` flag that `isLinkCurrent` reads (even though `test` mode returns +`null`), and `Application::createErrorRequest` after a forward. + +## The 4xx-not-500 contract + +A malformed parameter must degrade to 4xx, not crash to 500, but only for +*untrusted* values. The mechanism is a **subtype**: +`InvalidRequestParameterException extends InvalidLinkException`. + +- `ParameterConverter::toParameters` throws the **subtype only for supplemental + values** — those pulled from the *current request's own params* when the + destination is `this` (`$refPresenter->getParameters()`). Explicit caller-supplied + args throw the plain parent `InvalidLinkException`. +- **`redirect`/`redirectPermanent`/`forward` catch only the subtype** → `error()` + (4xx); **`isLinkCurrent` catches the subtype** → `false`. +- **`link`/`canonicalize` catch the parent** `InvalidLinkException` → the subtype + passes through as before → `#error` / silently abandoned canonical redirect. + +The net effect: a hostile `?param=` re-fed through `redirect('this')` in +`startup()` yields 4xx, while a programmer's typo in an explicit link argument still +surfaces as an exception. The subtype is BC-safe because it passes existing +`catch (InvalidLinkException)` blocks. + +`$invalidLinkMode` (bit flags `Silent`/`Warning`/`Exception`/`Textual`) decides what +`handleInvalidLink` does with an escaped `InvalidLinkException` in the `link` path +(`#`, `E_USER_WARNING`, rethrow, or `#error: `). + +## Two `link()`s + +`Presenter` has **no** `link()` of its own — it inherits `Component::link()`, which +delegates to the presenter's `LinkGenerator` in `link` mode and **wraps exceptions +via `handleInvalidLink` (respecting `$invalidLinkMode`)**. The standalone +`LinkGenerator::link()` (constructed with its own router + reference URL) does +**not** swallow exceptions — it always throws — and is what you use to generate +links outside a running presenter. Its default `$mode = null` also means the URL +is always **absolute** (relativization applies only to an explicit `'link'` mode). + +The reference URL is the HTTP request's URL (`ApplicationExtension` wires +`@Nette\Http\IRequest::getUrl`), and routers read only its host, base path and +path from it, never the script name. Outside an HTTP request (cron, CLI) the +request has no host of its own; nette/http fills it from `http: baseUrl`, so +this package needs no option for that. `withReferenceUrl()` is the per-call +override for multi-domain setups. diff --git a/docs/internals/persistent-state.md b/docs/internals/persistent-state.md new file mode 100644 index 000000000..a3687d12b --- /dev/null +++ b/docs/internals/persistent-state.md @@ -0,0 +1,58 @@ +# Persistent parameters & global state + +Persistent parameters survive across requests by riding in every generated URL. +The mechanism spans the whole component tree and has a strict validation contract. + +## Declaration and validation + +`#[Persistent]` on a **public, non-static** property is discovered by +`ComponentReflection::getParameters()`, which records its type and — for a +presenter — a `since` (the declaring class, used to scope which links may carry it). + +- **`loadState()`** converts each incoming param via + `ParameterConverter::convertType`; a **failure calls `error()` → 404** (a bad URL + value is client error, not a crash). Conversion is **lossless** for scalars (it + compares the round-tripped string), and **`callable` is deliberately rejected** + ("for security reasons"). Null params are ignored (the default stands). +- **`saveState()`/`saveStatePartial()`** re-convert on the way out; a failure here + throws `InvalidLinkException` (link generation, not a request), and a value equal + to its default is dropped (`null`) so it does not bloat the URL. + +## `#[Parameter]` rides the input path only + +`ComponentReflection::getParameters()` returns both `#[Persistent]` and +`#[Parameter]` properties, so `loadState()` fills and type-validates **both** the +same way (a bad value → `error()` → 404). The asymmetry is on the way out: only +persistent entries carry a `since` key, and `getPersistentParams()` filters on its +presence — a `#[Parameter]` property is never written back by `saveState` and never +reaches a URL. + +## Global state is aggregated across the whole tree + +`getGlobalState()` builds the full persistent-state map that links carry: + +1. unconsumed URL params in `globalParams` (prefixed by `id . NameSeparator`); +2. the presenter's own `saveStatePartial`; +3. a walk of the **entire `getComponentTree()`** — each `StatePersistent` component + contributes `saveState`, prefixed by its `getUniqueId() . NameSeparator`, while a + `sinces` table records where each key was declared; +4. when a `forClass` is given, keys whose `since` class is not among the target's + classes/traits are filtered out — so a link only carries state the destination + can actually receive. + +**`saveGlobalState()` empties `globalParams` (consuming them) and caches the +result.** It runs at the end of `run()` (always, since the terminating +`AbortException` lands after it) and again inside `Component::redirect()` before the +link is built. Global state reaches the URL because `createRequest` does `$args += +$refPresenter->getGlobalState(...)`. When a component attaches, `popGlobalParameters` +hands it its slice of the URL params so its own `loadState` runs. + +## The `_fid` flash key + +Flash messages survive a redirect via a session section keyed by a short random id +stored in the `FlashKey` (`_fid`) parameter. `hasFlashSession()` is true only when +`_fid` is a non-empty param **and** the session section `Nette.Application.Flash/` +exists. Consequently `_fid` is appended to a link **only in `redirect`/`forward` +mode and only when a flash session is already live** — ordinary links, and +`redirectX`, never carry it. `getFlashSession()` lazily generates the id (storing it +into the presenter's params) the first time a flash is written. diff --git a/docs/internals/readme.md b/docs/internals/readme.md new file mode 100644 index 000000000..0bc0b31a9 --- /dev/null +++ b/docs/internals/readme.md @@ -0,0 +1,21 @@ +# Application internals + +How `nette/application` (the MVC layer) works underneath, for agents editing it. +The emergent models don't share much context, so they're split by seam: + +- **[lifecycle.md](lifecycle.md)** — `Presenter::run()`, the exact phase order, + `AbortException`, signal dispatch, and canonicalization. +- **[link-generation.md](link-generation.md)** — `LinkGenerator::createRequest`, + the mode strings (including the real `redirectX`), destination parsing, + `$lastRequest`, and the 4xx-not-500 contract. The densest, trickiest code. +- **[persistent-state.md](persistent-state.md)** — persistent parameters, + `loadState`/`saveState`, whole-tree global-state aggregation, and the `_fid` + flash key. +- **[access-control.md](access-control.md)** — `AccessPolicy`, `#[Requires]`, the + three enforcement sites, and the implicit same-origin (CSRF) rule for signals. +- **[snippets.md](snippets.md)** — AJAX partial rendering: invalidation flags, the + snippet-mode render, and the payload contract. +- **[routing.md](routing.md)** — the Application `Route`/`RouteList` subclasses, + the `Nette:Micro` firewall exception, and `PresenterFactory`. +- **[templates.md](templates.md)** — template/layout file lookup and typed + templates. diff --git a/docs/internals/routing.md b/docs/internals/routing.md new file mode 100644 index 000000000..117e22321 --- /dev/null +++ b/docs/internals/routing.md @@ -0,0 +1,60 @@ +# Routing (Application layer) + +`nette/application` subclasses the `nette/routing` engine to add presenter/action/ +module conventions. The heavy lifting (mask parsing, the generation index / +`warmupCache`, nested-list `getConstantParameters` aggregation) lives in the base +package — see `nette/routing`'s internals. What is **here**: + +## `Application\Routers\Route` (over the base `Route`) + +- **`UIMeta` defaults** — injects `module`/`presenter`/`action` metadata with + kebab-case `FilterIn`/`FilterOut` (merged into `defaultMeta` before the parent + constructor). This is where a URL segment like `product-edit` becomes the + `ProductEdit` presenter. +- **kebab-case inflectors** — `action2path`/`path2action` (camelCase ↔ dashes) and + `presenter2path`/`path2presenter` (`Foo:Bar` ↔ `foo.bar`, `:` ↔ `.`). +- **Closure metadata → `Nette:Micro`** — a `\Closure` target is rewritten to + `['presenter' => 'Nette:Micro', 'callback' => $closure]`. This is the closure-route + mechanism, and the reason `Nette:Micro` must stay reachable (below). +- **`` handling** — after a base match, a `module` mask parameter is + prepended to the presenter (`module:Presenter`) and unset; `constructUrl` splits it + back out; `getConstantParameters` re-joins `module:presenter`. + +## `RouteList::completeParameters` and module scoping + +The Application `RouteList` adds module scoping on match: `completeParameters` +**prepends the list's `module` to the matched presenter — unless it starts with +`Nette:`**. `constructUrl` is the mirror: with a `module` set, the target presenter +must start with that prefix (else `null` — not this list's route), and the prefix +is stripped before delegating down. `withModule($m)` creates a nested list whose +`module` is `$m.':'` (a whole group under a module), which is distinct from the +per-route `` mask parameter above. `warmupCache` is *invoked* from `RoutingExtension::afterCompile` +(only when `routing: cache`), but is defined in the base package. + +## The `Nette:Micro` firewall exception + +`Application::createInitialRequest` **rejects any incoming request to a `Nette:*` +presenter except `Nette:Micro`**: `str_starts_with($presenter, 'Nette:') && +$presenter !== 'Nette:Micro'` → `BadRequestException`. Framework-internal presenters +must not be directly reachable from the network, but `Nette:Micro` is the legitimate +target that closure routes produce (mapped to `NetteModule\MicroPresenter`), so it +is the single allowed exception. `completeParameters`' `Nette:`-prefix skip exists +for the same reason — module scoping must not be applied to it. + +## Request loop and error routing + +`processRequest` has a `maxLoop` (default 20) guard, rejects direct requests **to** +the error presenters (only a forward may reach them), and turns an +`InvalidPresenterException` into a `BadRequestException` only on the first request. +`createErrorRequest` routes a `BadRequestException` to `error4xxPresenter ?? +errorPresenter` and anything else to `errorPresenter` (5xx); `sendHttpCode` maps a +`BadRequestException` to its HTTP code (else 404) and everything else to 500. + +## `PresenterFactory` + +`formatPresenterClass` maps a presenter name to a class through the mapping masks +(the hard default is `App\Presentation\*\**Presenter`; `setMapping` overrides), by +substituting `**`→`Part\Part` and `*`→`Part` per `:`-separated segment. Names are +charset-validated, the class is checked for `IPresenter`/non-abstract, and resolved +classes are cached. Aliases are resolved in the **link** layer (the `@alias` +destination), not in routing. diff --git a/docs/internals/snippets.md b/docs/internals/snippets.md new file mode 100644 index 000000000..b15385af6 --- /dev/null +++ b/docs/internals/snippets.md @@ -0,0 +1,46 @@ +# Snippets & AJAX partial rendering + +The snippet mechanism is emergent across three files: `Control` (invalidation +flags), the post-lifecycle block in `Presenter::run()` (the re-send), and +`SnippetRuntime` in the Latte bridge (the capture). None of them shows the whole +picture. + +## The pipeline + +1. During the lifecycle, `redrawControl()` marks a control (key `"\0"`) or a named + snippet as invalid. +2. After the terminating `AbortException` lands in `run()`, the AJAX block sets + `payload->state`, and — when the response is a `TextResponse` and something is + invalid — flips `snippetMode = true` and sends the `TextResponse` **right + there**. Since `sendTemplate` only stores the response and aborts, this is the + template's **only** render, and it happens in snippet mode. Then `sendPayload()` + replaces `$this->response` with a `JsonResponse` of the payload — that is what + the presenter returns to `Application`. +3. Every compiled template starts with a prolog injected by + `UIExtension::snippetRenderingPass`: it calls + `SnippetRuntime::renderSnippets(...)` and **returns early when it rendered** — + in snippet mode the main template body never runs. +4. `renderSnippets` renders only the invalid snippet blocks (into + `payload->snippets[]`, captured via output buffering in `enter`/`leave`), + then `renderChildren` walks the component tree and recursively renders every + invalid child `Control` with its `snippetMode` on. + +## Invariants & traps + +- **Invalidation propagates upward:** `isControlInvalid()` with no argument walks + the whole `Renderable` subtree — an invalid child makes the parent report + invalid. A named query falls back to the whole-control `"\0"` flag. +- **Rendering a snippet clears its flag:** `SnippetRuntime::enter()` calls + `redrawControl($name, redraw: false)`. Re-rendering the same snippet twice in one + request therefore captures it once. +- **Dynamic snippets are valid only inside a static `{snippet}`/`{snippetArea}`** + (`E_USER_WARNING` otherwise). A `snippetArea` only relays: its own wrapper output + is discarded, only inner snippets are captured. +- **The payload contract** (what nette/naja-style clients consume): `snippets`, + `state` (always set on AJAX — the global persistent state, see + persistent-state.md), and `redirect` — on AJAX, `redirectUrl()` does not send an + HTTP redirect but puts the URL into `payload->redirect` and sends the payload. +- Snippet element ids are `snippet--` (`getSnippetId`). +- `{snippet}`/`{snippetArea}` compile to `enter`/`leave` calls + (`Nodes/SnippetNode.php`); the driver instance comes from the `snippetDriver` + provider and exists only when the template has a control. diff --git a/docs/internals/templates.md b/docs/internals/templates.md new file mode 100644 index 000000000..0f2bfd2a5 --- /dev/null +++ b/docs/internals/templates.md @@ -0,0 +1,56 @@ +# Template & layout lookup + +Templates are found by convention; the candidate paths and their order are the only +non-obvious part. + +## Action template candidates (`formatTemplateFiles`) + +Starting from the presenter's own directory: + +- **If there is no `templates/` subdirectory**, the lookup climbs one level; if + there is still none, the single candidate is `"/.latte"` (the + "template next to the presenter" layout). +- **If a `templates/` subdirectory exists**, two candidates are tried, in order: + 1. `"/templates//.latte"` + 2. `"/templates/..latte"` + +`findTemplateFile()` returns the first that `is_file`, else calls `error()` → **404 +"Missing template"**. Remember (see lifecycle.md) this resolution happens **lazily +at `sendTemplate`**, via `completeTemplate`, only when the template has no explicit +file. + +## Layout candidates (`formatLayoutTemplateFiles`) + +- A `$layout` containing a slash (or backslash) is taken as a direct path. +- Otherwise the base name is `` (default `layout`). With a `templates/` + directory the candidates are `templates//@.latte` and + `templates/.@.latte` (once, at the presenter's level only), + then the shared `templates/@.latte` repeated **up the module levels** + (`substr_count(name, ':')` levels of `dirname`). Without a `templates/` + directory, `@.latte` next to the presenter, then walking up the same way. +- `setLayout(false)` disables the layout entirely (returns `null`); an explicitly + named layout that is not found throws `FileNotFoundException`, while the default + layout being absent is simply `null` (optional). + +## Typed templates + +`formatTemplateClass` derives the template class from the presenter/control name: +for a presenter it tries `Template` then `Template` (stripping +the `Presenter` suffix); for a control, `Template` (stripping `Control`). The +class is validated (`is_a(..., Template::class)`) or a `trigger_error` degrades to +the default. `TemplateFactory::createTemplate` instantiates it around a fresh Latte +engine and injects the default variables (`user`, `baseUrl`, `basePath`, `flashes`, +`control`, `presenter`). + +## `#[TemplateVariable]` + +At `sendTemplate`, `completeTemplate()` copies every `#[TemplateVariable]` property +of the presenter into the template via `$template->$name ??= $this->$name` — an +explicitly assigned template variable wins. The property must be **public** +(`LogicException` otherwise); uninitialized properties are skipped (unless they +have a get hook, PHP 8.4+). Presenter-only — plain controls don't run +`completeTemplate`. + +The `{templateType}` Latte tag is **not** in this package — it is implemented in +Latte core; here the typing is only the name-derived class plus the +`@property-read Template $template` docblock. From f1d5818f24b9c162e1875483e7c082bd5e03976c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 24 Aug 2026 13:19:33 +0200 Subject: [PATCH 10/54] added upgrading.md --- AGENTS.md | 6 + docs/upgrading.md | 371 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 docs/upgrading.md diff --git a/AGENTS.md b/AGENTS.md index 40665db71..bab0d171b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,12 @@ The presenter lifecycle, link generation, persistent state, routing, and templat lookup carry subtle non-local invariants - read `docs/internals` before touching them. +`docs/upgrading.md` records, per released version, every change visible to +users of the package (renames, removals, deprecations, signatures, behavior, +configuration), each with its commit hash; internal changes are not in it. It +is the place to look up the history of a public symbol. Do not write to it; it +is extended at release time from the git history. + ## Project Overview Nette Application is a full-stack, component-based MVC framework library for PHP - diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 000000000..d5768adb9 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,371 @@ +# Upgrading nette/application + +## 3.3.0 + +- **Removed** `Nette\Bridges\ApplicationLatte\UIMacros`: Latte 2 is no longer supported, the tags are provided by `Nette\Bridges\ApplicationLatte\UIExtension` under Latte 3 (92532c24) +- **Removed** `{ifCurrent}` (deprecated since 3.1.6): use `{if isLinkCurrent('destination')}` (d75c3057) +- **Renamed** `Nette\Application\UI\PresenterComponent` → `Nette\Application\UI\Component`; the old name was removed (3432ff34) +- **Renamed** `Nette\Application\UI\PresenterComponentReflection` → `Nette\Application\UI\ComponentReflection`; the old name was removed (3432ff34) +- **Renamed** `Nette\Application\IRouter` → `Nette\Routing\Router` (alias since 3.1.0); the old name was removed (3432ff34) +- **Renamed** `@persistent` → `#[Nette\Application\Attributes\Persistent]`; using the old name triggers E_USER_DEPRECATED (2b611739) +- **Renamed** `@deprecated` → `#[Nette\Application\Attributes\Deprecated]`; using the old name triggers E_USER_DEPRECATED (2b611739) +- **Replaced** `@crossOrigin` with `#[Nette\Application\Attributes\Requires(sameOrigin: false)]`: using the annotation triggers E_USER_DEPRECATED (2b611739) +- **Replaced** `Nette\Application\Routers\RouteList::offsetSet()` with `Nette\Application\Routers\RouteList::addRoute()` for a route and `Nette\Application\Routers\RouteList::add()` for any other router: appending via `$routeList[] =` triggers E_USER_DEPRECATED; replacing the router at an index via `$routeList[$index] =` has no public replacement, `Nette\Routing\RouteList::modify()` being protected, so add the routers in the order they are to be matched (82c16085) +- **Replaced** `Nette\Application\Routers\RouteList::offsetGet()` with `Nette\Application\Routers\RouteList::getRouters()`: reading via `$routeList[$index]` triggers E_USER_DEPRECATED (82c16085) +- **Deprecated** `Nette\Application\Routers\RouteList::offsetUnset()`: `unset($routeList[$index])` triggers E_USER_DEPRECATED and has no public replacement, `Nette\Routing\RouteList::modify()`, which the deprecation notice names, being protected; leave the router out when the list is built, or call `modify($index, null)` from a subclass (82c16085) +- **Deprecated** `Nette\Application\Routers\RouteList::offsetExists()`: check `Nette\Application\Routers\RouteList::getRouters()` instead; using the old name triggers E_USER_DEPRECATED (82c16085) +- **Deprecated** `Nette\Application\UI\Component::$presenter`: use `Nette\Application\UI\Component::getPresenter()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Component::$linkCurrent`: use `Nette\Application\UI\Component::isLinkCurrent()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$action`: use `Nette\Application\UI\Presenter::getAction()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$view`: use `Nette\Application\UI\Presenter::getView()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$layout`: use `Nette\Application\UI\Presenter::getLayout()` and `Nette\Application\UI\Presenter::setLayout()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$request`: use `Nette\Application\UI\Presenter::getRequest()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$session`: use `Nette\Application\UI\Presenter::getSession()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$method`: use `Nette\Application\Request::getMethod()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$presenterName`: use `Nette\Application\Request::getPresenterName()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$parameters`: use `Nette\Application\Request::getParameters()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$post`: use `Nette\Application\Request::getPost()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$files`: use `Nette\Application\Request::getFiles()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `$default` of `Nette\Application\UI\Component::getParameter()`: use `getParameter($name) ?? $default`; passing it triggers E_USER_DEPRECATED (6a10e27a) +- **Deprecated** a query string in a link destination, e.g. `link('Presenter:action?foo=bar')`: pass the parameters as arguments, e.g. `link('Presenter:action', ['foo' => 'bar'])`; using the old form triggers E_USER_DEPRECATED (b6da1870) +- **Deprecated** `Nette\Application\UI\Control::templatePrepareFilters()` (silently deprecated since 3.1.8): it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Deprecated** `Nette\Application\UI\Form::disableSameSiteProtection()` (silently deprecated since 3.0.8): use `#[Nette\Application\Attributes\Requires(sameOrigin: false)]`; it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Deprecated** `Nette\Application\UI\Presenter::createRequest()` (deprecated since 3.2.3): use `Nette\Application\LinkGenerator`; it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Deprecated** `Nette\Application\UI\Presenter::parseDestination()` (deprecated since 3.2.3): use `Nette\Application\LinkGenerator::parseDestination()`; it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Deprecated** `Nette\Application\UI\Presenter::requestToUrl()` (deprecated since 3.2.3): use `Nette\Application\LinkGenerator::requestToUrl()`; it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Signature** `Nette\Application\Application::processRequest()` returns `Nette\Application\Response` instead of `void` and no longer sends it; the caller must call `send()` on the returned response (e0192d23) +- **Signature** `Nette\Bridges\ApplicationLatte\LatteFactory::create()` takes `?Nette\Application\UI\Control $control = null`; a custom implementation must accept it and pass it to `Nette\Bridges\ApplicationLatte\UIExtension` (bed96a1c) +- **Signature** `Nette\Application\UI\Presenter::getRequest()` returns `Nette\Application\Request` instead of `?Nette\Application\Request` and throws `Nette\InvalidStateException` when the presenter is not running (35c9c679) +- **Signature** `$signal` of `Nette\Application\UI\Presenter::isSignalReceiver()` is `string|true|null`; passing `false` now throws a `TypeError` (230381ee) +- **Signature** `$name` of `Nette\Bridges\ApplicationLatte\Template::addFilter()` is no longer nullable (fe26ff9b) +- **Signature** `$cacheStorage` of `Nette\Bridges\ApplicationLatte\TemplateFactory::__construct()` was removed; the Latte cache is set up by the caching extension instead (92532c24) +- **Changed** the default presenter mapping is `App\Presentation\*\**Presenter`; previously `*Module\*Presenter`; set `application.mapping` explicitly to keep the previous structure (30d82cfc) +- **Changed** `Nette\Application\LinkGenerator::link()` takes the `#` key of the arguments as the URL fragment; previously it was passed on as an ordinary parameter, and a non-scalar value now throws `Nette\Application\UI\InvalidLinkException` (e3dabf20) +- **Changed** `Nette\Bridges\ApplicationLatte\TemplateFactory::createTemplate()` no longer adds `Nette\Bridges\ApplicationLatte\UIExtension` when the Latte factory did not; a custom `Nette\Bridges\ApplicationLatte\LatteFactory` must register it itself (bed96a1c) +- **Changed** a presenter that is not registered as a service in the DI container is no longer created dynamically; in development mode with `touchToRefresh` the page is refreshed, otherwise `Nette\Application\InvalidPresenterException` is thrown (4abfb482) +- **Changed** snippet redrawing recognizes only descendants of `Nette\Application\UI\Control`; a component implementing merely `Nette\Application\UI\Renderable` is no longer redrawn (fe26ff9b) +- **Config** `latte.macros`: removed, register Latte 3 extensions under `latte.extensions` (92532c24) + +## 3.2.12 + +- **Changed** `Nette\Application\UI\Component::redirect()` calls `error()` (4xx) when a value carried over from the current request's own persisted parameters cannot be coerced to the target action's parameter type; previously `Nette\Application\UI\InvalidLinkException` propagated and ended as a 500 (8d393b85) +- **Changed** `Nette\Application\UI\Component::isLinkCurrent()` returns false when a value carried over from the current request's own persisted parameters cannot be coerced to the target action's parameter type; previously the exception propagated (8d393b85) +- **Changed** `Nette\Application\UI\Presenter::forward()` calls `error()` (4xx) when a value carried over from the current request's own persisted parameters cannot be coerced to the target action's parameter type; previously the exception propagated and ended as a 500 (8d393b85) + +## 3.2.10 + +- **Deprecated** `Nette\Application\UI\Component::getPresenterIfExists()`: use `getPresenter(throw: false)`; the old name is silently deprecated (893fdcc7) +- **Deprecated** `Nette\Application\UI\Form::getPresenterIfExists()`: use `getPresenter(throw: false)`; the old name is silently deprecated (893fdcc7) +- **Deprecated** `Nette\Application\UI\Presenter::getPresenterIfExists()`: use `getPresenter(throw: false)`; the old name is silently deprecated (893fdcc7) +- **Signature** the `$throw` parameter is back in `Nette\Application\UI\Component::getPresenter()`, `Nette\Application\UI\Form::getPresenter()` and `Nette\Application\UI\Presenter::getPresenter()` and passing it no longer triggers E_USER_DEPRECATED (893fdcc7) +- **Signature** `$contentType` of `Nette\Application\Responses\FileResponse::__construct()` is `string` with the default `'application/octet-stream'`; passing `null` now throws a `TypeError` (dced88ca) +- **Signature** `$contentType` of `Nette\Application\Responses\JsonResponse::__construct()` is `string` with the default `'application/json'`; passing `null` now throws a `TypeError` (dced88ca) +- **Signature** `NetteModule\MicroPresenter::error()` returns `never` instead of `void` (f3f28851) +- **Changed** `{layout auto}` in a referring template no longer calls `Nette\Application\UI\Presenter::findLayoutTemplateFile()` and resolves to no layout there (cdf59b0a) + +## 3.2.9 + +- **Removed** `Nette\Bridges\ApplicationLatte\Template::blueprint()` (0044598c) +- **Deprecated** `Nette\Application\Attributes\CrossOrigin`: use `#[Nette\Application\Attributes\Requires(sameOrigin: false)]`; the old name is silently deprecated (75fb3f4d) +- **Changed** `Nette\Application\UI\Component::link()` throws `Nette\Application\UI\InvalidLinkException` when the target is marked `#[Nette\Application\Attributes\Requires(forward: true)]`, unless the link is built while forwarding (1b0aa552) + +## 3.2.8 + +- **Changed** `Nette\Application\UI\Presenter::$invalidLinkMode` configured in the DI container is now really applied to presenters created outside it; previously it stayed silent, so warnings or exceptions for invalid links may newly appear (74e5d82f) +- **Changed** `Nette\Application\LinkGenerator::createRequest()` no longer passes parameters marked `#[Nette\Application\Attributes\Parameter]` whose value equals the default (56986e64) + +## 3.2.7 + +- **Changed** `Nette\Bridges\ApplicationLatte\Template` is abstract; instantiate `Nette\Bridges\ApplicationLatte\DefaultTemplate` or another subclass (8f567030) +- **Changed** `Nette\Bridges\ApplicationLatte\TemplateFactory::createTemplate()` silently skips a default variable (user, baseUrl, basePath, flashes, control, presenter) whose type does not match the template property; previously it failed with a `TypeError` (edf6c279) + +## 3.2.4 + +- **Signature** `Nette\Application\UI\Form::beforeRender()` returns `void`; affects subclasses overriding it (c0d041fe) +- **Changed** container compilation throws `Nette\InvalidStateException` when `#[Nette\Application\Attributes\Deprecated]` is used on a method other than action, render or handle (630ebb14) +- **Changed** container compilation throws `Nette\InvalidStateException` when `#[Nette\Application\Attributes\Parameter]` or `#[Nette\Application\Attributes\Persistent]` is used on a property that is not public, non-static and non-readonly (630ebb14) +- **Changed** container compilation throws `Nette\InvalidStateException` when `#[Nette\Application\Attributes\Requires]` is used on a method other than action, render, handle or createComponent (630ebb14) +- **Changed** container compilation throws `Nette\InvalidStateException` when an action, render or signal method of a presenter is not public and non-static (630ebb14) +- **Changed** container compilation throws `Nette\InvalidStateException` when a `createComponent()` method is private or static (630ebb14) + +## 3.2.3 + +- **Replaced** `Nette\Application\UI\Presenter::createRequest()` with `Nette\Application\LinkGenerator::createRequest()`: link building moved to the `Nette\Application\LinkGenerator` service, reachable via `Nette\Application\UI\Presenter::getLinkGenerator()`, so overriding the presenter method no longer affects generated links (850ab855) +- **Deprecated** `Nette\Application\UI\Presenter::$allowedMethods`: use `#[Nette\Application\Attributes\Requires(methods: [...])]` on the presenter class or on the action; the property is silently deprecated (368ae99f) +- **Deprecated** `Nette\Application\UI\Presenter::checkHttpMethod()`: use `#[Nette\Application\Attributes\Requires(methods: [...])]`; the method is silently deprecated (368ae99f) +- **Signature** `$element` of `Nette\Application\UI\Component::checkRequirements()` is typed `\ReflectionClass|\ReflectionMethod`; affects subclasses overriding it (7af4f067) +- **Signature** `Nette\Application\LinkGenerator::link()` is `link(string $destination, array $args = [], ?Nette\Application\UI\Component $component = null, ?string $mode = null): ?string`; the return type is nullable and the first parameter was renamed from `$dest` to `$destination` (51584954) +- **Signature** `$reflection` of `Nette\Application\UI\Presenter::saveState()` was removed; a second argument is now silently ignored (b644c0d4) +- **Signature** `Nette\Application\Attributes\Parameter` and `Nette\Application\Attributes\Persistent` are final and can no longer be extended (5d5f8d18) +- **Changed** `Nette\Application\UI\Component::checkRequirements()` no longer performs the same-origin check for signals; the check runs before it is called, so an override that does not call the parent no longer bypasses it (7af4f067) +- **Changed** `Nette\Application\UI\Component::redirectPermanent()` no longer saves the global state of subcomponents before redirecting; only `Nette\Application\UI\Component::redirect()` does (bb652c12) +- **Changed** `Nette\Application\LinkGenerator::link()` resolves the target action or render method only when it is callable (public, non-static, non-abstract); previously any existing method matched (b309aa94) +- **Changed** `Nette\Application\UI\Presenter::formatActionMethod()` returns `'actionDefault'`; previously `'actiondefault'` (def38b35) +- **Changed** `Nette\Application\UI\Presenter::formatRenderMethod()` returns `'renderDefault'`; previously `'renderdefault'` (def38b35) +- **Changed** `Nette\Application\UI\Presenter::getPersistentComponents()` returns an empty array by default; names declared by the attribute are merged elsewhere, so an override no longer has to call the parent to keep them (d95272f3) +- **Changed** `{templatePrint}` requires latte/latte 3.0.15 or newer and throws `\LogicException` otherwise (7f094568) +- **Changed** `|translate` used without `Nette\Bridges\ApplicationLatte\Template::setTranslator()` ends with Latte's undefined-filter compile error; previously `Nette\InvalidStateException` at render time (c86bdbef) +- **Changed** `latte.extensions`: the cache and forms extensions are registered before the user-configured ones; previously afterwards, which changes precedence of tags and filters defined under the same name by more than one extension (3a4f4017) +- **Config** `application.catchExceptions`: the value `'4xx'` introduced in 3.2.1 was removed, only `true` and `false` are accepted (56be3de3) + +## 3.2.1 + +- **Replaced** `Nette\Application\Application::processException()` with `Nette\Application\Application::createErrorRequest()`: the new method returns a `Nette\Application\Request` (or null) that the caller must pass to `Nette\Application\Application::processRequest()` and no longer sends the response itself (18a38043) +- **Deprecated** `Nette\Application\Application::$catchExceptions`: exceptions are caught whenever `Nette\Application\Application::$errorPresenter` is set; the property is silently deprecated (133d0e7f) +- **Changed** `Nette\Application\Application::$catchExceptions` defaults to `true`; previously `false` (133d0e7f) +- **Changed** `Nette\Application\UI\Presenter::injectPrimary()` called a second time throws a native `\Error` because the injected properties are readonly; previously `Nette\InvalidStateException` (bb59a51b) +- **Config** `application.catchExceptions` also accepts the string `'4xx'` and its default changed to `'4xx'`; previously `false` (d16847f4) + +## 3.2.0 + +- **Removed** `Nette\Application\UI\Presenter::getContext()` (deprecated since 3.1.0): inject the services you need explicitly (75912403) +- **Removed** `Nette\Application\Request::SECURED` (573cd9b9) +- **Removed** `Nette\Application\Routers\Route::getFlags()` (deprecated since 3.1.0) (75912403) +- **Removed** `Nette\Application\Routers\SimpleRouter::getFlags()` (deprecated since 3.1.0) (75912403) +- **Removed** `Nette\Application\Routers\RouteList::count()` (deprecated since 3.1.0): the class no longer implements `Countable`, count `Nette\Application\Routers\RouteList::getRouters()` instead (75912403) +- **Replaced** `Nette\Application\Routers\RouteList::getIterator()` with `Nette\Application\Routers\RouteList::getRouters()`: the class no longer implements `IteratorAggregate`, iterate over the returned array (75912403) +- **Replaced** `$flags` of `Nette\Application\Routers\Route::__construct()` with `$oneWay` of `Nette\Application\Routers\RouteList::addRoute()` (75912403) +- **Replaced** `$flags` of `Nette\Application\Routers\SimpleRouter::__construct()` with the second argument of `Nette\Application\Routers\RouteList::add()` (75912403) +- **Signature** `Nette\Application\UI\Presenter::injectPrimary()` no longer takes `Nette\DI\Container` and the parameter order is now httpRequest, httpResponse, presenterFactory, router, session, user, templateFactory; affects positional calls (bb8f93c6) +- **Signature** `Nette\Application\UI\Control::createTemplate()` and `Nette\Application\UI\Presenter::createTemplate()` take `?string $class = null`; affects subclasses overriding them (e2c2e74b) +- **Signature** property types were added: `Nette\Application\Application::$catchExceptions` is `bool` with default false, `Nette\Application\UI\Control::$snippetMode` is `bool` with default false and `Nette\Application\UI\Presenter::$invalidLinkMode` is `int` with default 0; assigning `null` throws a `TypeError` and an unset value can no longer be recognized by a null check (244fc48a) +- **Signature** `Nette\Bridges\ApplicationLatte\DefaultTemplate::$presenter` is typed `Nette\Application\IPresenter` instead of `Nette\Application\UI\Presenter` (244fc48a) +- **Signature** the third parameter of `Nette\Application\Routers\RouteList::addRoute()` was renamed from `$flags` to `$oneWay` and typed `int|bool`; affects calls using named arguments (b3d618a0) +- **Changed** the `Nette\SmartObject` trait was removed from `Nette\Application\Application`, `Nette\Application\PresenterFactory`, `Nette\Application\LinkGenerator`, `Nette\Application\UI\Link`, all classes in `Nette\Application\Responses`, `Nette\Application\Routers\CliRouter`, `Nette\Bridges\ApplicationLatte\TemplateFactory`, `Nette\Bridges\ApplicationTracy\RoutingPanel`, `NetteModule\ErrorPresenter` and `NetteModule\MicroPresenter`; reading an undefined property or calling an undefined method raises a native PHP error instead of `Nette\MemberAccessException`, and writing an undefined property is no longer blocked (a472b2e0) +- **Changed** `Nette\Application\UI\Form` whose action was changed by `setAction()` no longer adds the hidden signal field in `beforeRender()`, so the signal submit handler is not triggered for such forms (8d8313a3) +- **Changed** `Nette\Application\Routers\SimpleRouter::__construct()` no longer throws `Nette\DeprecatedException` for a `module` key in `$defaults`; the key is accepted as an ordinary default, use `Nette\Application\Routers\RouteList::withModule()` to scope a module (75912403) +- **Config** `application.catchExceptions` defaults to `false`; previously `null` (244fc48a) +- **Config** `latte.xhtml`: removed, using it throws an exception (75912403) +- **Config** `routing.routeClass`: removed, using it throws an exception (75912403) + +## 3.1.15 + +- **Changed** `Nette\Application\AbortException` extends `\LogicException` instead of `\Exception`; affects code catching it (00da68b6) +- **Changed** `Nette\Application\BadRequestException` extends `\LogicException` instead of `\Exception`; affects code catching it (00da68b6) +- **Changed** `Nette\Application\UI\Component::tryCall()` reports a handler that exists but is not callable through `error()`, i.e. `Nette\Application\BadRequestException` (404); previously `Nette\InvalidStateException`, also in 3.2.4 (dafa250c) + +## 3.1.14 + +- **Replaced** `Nette\Application\Routers\RouteList::match()` with `Nette\Application\Routers\RouteList::completeParameters()`: a subclass that overrode match() to complete the matched parameters now implements `protected function completeParameters(array $params): ?array`, which the parent class calls (7f871545) +- **Changed** `NetteModule\MicroPresenter::run()` autowires only callback parameters typed with a class or interface; a parameter with a builtin type keeps the value coming from the request instead of being overwritten by the container lookup (c634aabe) + +## 3.1.13 + +- **Changed** `Nette\Application\UI\Presenter::run()` rejects an HTTP method not listed in the new `Nette\Application\UI\Presenter::$allowedMethods` (GET, POST, HEAD, PUT, DELETE) with a 405 error; set the property to allow other methods (83f51448) + +## 3.1.11 + +- **Renamed** `Nette\Application\UI\Presenter::INVALID_LINK_SILENT` → `Nette\Application\UI\Presenter::InvalidLinkSilent`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::INVALID_LINK_WARNING` → `Nette\Application\UI\Presenter::InvalidLinkWarning`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::INVALID_LINK_EXCEPTION` → `Nette\Application\UI\Presenter::InvalidLinkException`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::INVALID_LINK_TEXTUAL` → `Nette\Application\UI\Presenter::InvalidLinkTextual`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::PRESENTER_KEY` → `Nette\Application\UI\Presenter::PresenterKey`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::SIGNAL_KEY` → `Nette\Application\UI\Presenter::SignalKey`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::ACTION_KEY` → `Nette\Application\UI\Presenter::ActionKey`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::FLASH_KEY` → `Nette\Application\UI\Presenter::FlashKey`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::DEFAULT_ACTION` → `Nette\Application\UI\Presenter::DefaultAction`; the old name still works as an alias (73391b9e) +- **Changed** an action, render or signal method must be public; a protected one throws `Nette\InvalidStateException` instead of being invoked as it was since 3.1.2 (3137e5df) + +## 3.1.10 + +- **Changed** `Nette\Application\UI\Presenter::detectedCsrf()` converts `Nette\Application\UI\InvalidLinkException` raised for an invalid current link into `Nette\Application\BadRequestException`; previously the InvalidLinkException propagated (f2a72dc8) + +## 3.1.8 + +- **Deprecated** `Nette\Application\UI\Control::templatePrepareFilters()`: the method is silently deprecated (65f01cc8) +- **Signature** `$default` of `Nette\Application\UI\Component::getParameter()` was removed from the signature; a second argument still works through `func_get_arg()` but should no longer be passed (83212fd2) + +## 3.1.7 + +- **Renamed** `Nette\Bridges\ApplicationLatte\TranslatorExtension` → `Latte\Essential\TranslatorExtension`; the old name was removed (a831a22c) + +## 3.1.6 + +- **Replaced** `{ifCurrent destination}` with `{if isLinkCurrent('destination')}`: under Latte 3 the tag triggers E_USER_DEPRECATED (2bfc034d) +- **Changed** a modifier other than `|noescape` in `{control}` is a compile exception under Latte 3; under Latte 2 it only triggered E_USER_DEPRECATED (2bfc034d) +- **Config** `latte.macros` has no effect under Latte 3, register extensions under `latte.extensions` (2bfc034d) + +## 3.1.5 + +- **Signature** `Nette\Application\UI\Component::link()`, `lazyLink()`, `redirect()`, `redirectPermanent()` and `isLinkCurrent()` take `$args = []` again instead of the variadic `...$args` introduced in 3.1.4 (b89b3d6a) + +## 3.1.4 + +- **Replaced** `{templatePrint default}` with `{templatePrint Nette\Bridges\ApplicationLatte\DefaultTemplate}`: the special `default` keyword was removed (1256ecf7) +- **Signature** `Nette\Application\UI\Component::link()`, `lazyLink()`, `redirect()`, `redirectPermanent()` and `isLinkCurrent()` take a variadic `...$args` instead of `$args = []` (a987d588) +- **Signature** `Nette\Application\UI\Presenter::canonicalize()` takes a variadic `...$args` instead of `array $args = []` (ce7df20e) +- **Changed** generating a link to a presenter, an action or a signal handler annotated `@deprecated` triggers E_USER_DEPRECATED when invalidLinkMode is enabled; previously it was silent (4422ae62) +- **Changed** `Nette\Application\UI\Presenter::run()` saves the global state and the AJAX payload state even when the request ends early through `forward()`, `redirect()` or `sendResponse()`; previously only after a normal render, so persistent parameters were not saved on early termination (db6eedc1) + +## 3.1.2 + +- **Changed** a protected action, render or signal method is invoked like a public one; previously it was silently treated as not found (06823f59) +- **Changed** a private action, render or signal method makes `Nette\Application\UI\Component::tryCall()` throw `Nette\InvalidStateException`; previously it was silently treated as not found (06823f59) +- **Changed** `Nette\Application\UI\Control::formatTemplateClass()` strips only a trailing "Control" from the class name and returns a name even for a class not ending with it; previously it also stripped "Presenter" and returned null in that case (0a4aeabc) +- **Changed** `Nette\Application\UI\Presenter::formatTemplateClass()` looks for a per-action template class (`FooBarTemplate` for action bar of `FooPresenter`) before falling back to the default one (55c9d45c, 0a4aeabc) + +## 3.1.1 + +- **Deprecated** `latte.xhtml`: using the key triggers E_USER_DEPRECATED (d6989a2e) +- **Changed** `Nette\Application\UI\Component::createComponent()` triggers E_USER_NOTICE when the created component implements neither `Nette\Application\UI\SignalReceiver` nor `Nette\Application\UI\StatePersistent` (d07e7fb3) +- **Changed** a custom template class (`latte.templateClass` or the constructor argument of `Nette\Bridges\ApplicationLatte\TemplateFactory`) must extend `Nette\Bridges\ApplicationLatte\Template`; merely implementing `Nette\Application\UI\Template` makes the factory throw `Nette\InvalidArgumentException` (4b4f2866) +- **Changed** `{control}` outside HTML text, e.g. inside an attribute, is allowed and the component output is escaped for that context; previously it triggered E_USER_WARNING (6d81a0e4) + +## 3.1.0 + +- **Removed** `Nette\Application\Routers\Route::$styles` (deprecated since 3.0.0) (9a161a29) +- **Removed** the `@User(loggedIn)` annotation on presenter action and render methods (deprecated since 3.0.0): check the user in `Nette\Application\UI\Presenter::checkRequirements()` instead (9a161a29) +- **Removed** `|null` (deprecated since 3.0.0) (9a161a29) +- **Removed** a modifier in `{templatePrint}`: using one now throws a compile exception (09ac4e3e) +- **Renamed** `|url` → `|escapeUrl`; the old name was removed (9a161a29) +- **Renamed** `Nette\Application\IRouter` → `Nette\Routing\Router` (silently deprecated since 3.0.0); the old name still works as an alias (d200f234) +- **Renamed** `Nette\Application\IResponse` → `Nette\Application\Response`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\IRenderable` → `Nette\Application\UI\Renderable`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\ISignalReceiver` → `Nette\Application\UI\SignalReceiver`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\IStatePersistent` → `Nette\Application\UI\StatePersistent`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\ITemplate` → `Nette\Application\UI\Template`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\ITemplateFactory` → `Nette\Application\UI\TemplateFactory`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Bridges\ApplicationLatte\ILatteFactory` → `Nette\Bridges\ApplicationLatte\LatteFactory`; the old name still works as an alias (8ffa8fcc) +- **Replaced** `Nette\Application\UI\Component::getParam()` (deprecated since 3.0.0) with `Nette\Application\UI\Component::getParameters()` when called without an argument and with `Nette\Application\UI\Component::getParameter()` when called with one (9a161a29) +- **Replaced** the numeric `$code` first argument of `Nette\Application\UI\Component::redirect()` (deprecated since 3.0.0) with `Nette\Application\UI\Component::redirectPermanent()` for 301; other codes have no replacement (9a161a29) +- **Replaced** the `module` key in `$defaults` of `Nette\Application\Routers\SimpleRouter::__construct()` with `Nette\Application\Routers\RouteList::withModule()`: passing it now throws `Nette\DeprecatedException` (9a161a29) +- **Replaced** `|normalize` with `Nette\Utils\Strings::normalize()` applied to the value (9a161a29) +- **Replaced** `|toAscii` with `Nette\Utils\Strings::toAscii()` applied to the value (9a161a29) +- **Replaced** `Nette\Bridges\ApplicationLatte\Template::add()` with `Nette\Bridges\ApplicationLatte\DefaultTemplate::add()`: the method moved to the subclass, extend DefaultTemplate if you need it (34c4a715) +- **Replaced** `Nette\Bridges\ApplicationLatte\Template::setParameters()` with `Nette\Bridges\ApplicationLatte\DefaultTemplate::setParameters()`: the method moved to the subclass (34c4a715) +- **Deprecated** `Nette\Application\UI\Presenter::getContext()`: inject the services you need explicitly; using it triggers E_USER_DEPRECATED (0d479ac0) +- **Deprecated** `$flags` of `Nette\Application\Routers\Route::__construct()`: pass the flag to `Nette\Application\Routers\RouteList::addRoute()`; passing it triggers E_USER_DEPRECATED (de327f3f) +- **Deprecated** `$flags` of `Nette\Application\Routers\SimpleRouter::__construct()`: pass the flag to `Nette\Application\Routers\RouteList::add()`; passing it triggers E_USER_DEPRECATED (de327f3f) +- **Deprecated** `Nette\Application\Routers\Route::getFlags()`: using it triggers E_USER_DEPRECATED (de327f3f) +- **Deprecated** `Nette\Application\Routers\SimpleRouter::getFlags()`: using it triggers E_USER_DEPRECATED (de327f3f) +- **Deprecated** `Nette\Application\Routers\RouteList::count()`: count `Nette\Application\Routers\RouteList::getRouters()`; using it triggers E_USER_DEPRECATED (46a15001) +- **Deprecated** `Nette\Application\Routers\RouteList::getIterator()`: iterate over `Nette\Application\Routers\RouteList::getRouters()`; using it triggers E_USER_DEPRECATED (46a15001) +- **Deprecated** a modifier in `{control}`: using one triggers E_USER_DEPRECATED (09ac4e3e) +- **Deprecated** `{control}` used outside HTML text: using it triggers E_USER_WARNING (6a187d1c) +- **Changed** `Nette\Bridges\ApplicationLatte\TemplateFactory` creates `Nette\Bridges\ApplicationLatte\DefaultTemplate` by default; previously `Nette\Bridges\ApplicationLatte\Template`, which no longer declares the presenter, control, user, baseUrl, basePath and flashes variables (34c4a715) +- **Changed** `Nette\Bridges\ApplicationLatte\Template` has no magic `__get`, `__set`, `__isset` and `__unset` and does not use `Nette\SmartObject`; parameters are ordinary public properties and reading an undefined one raises a PHP warning instead of E_USER_NOTICE (450720cb) +- **Changed** `Nette\Bridges\ApplicationLatte\DefaultTemplate::add()` throws `Nette\InvalidStateException` on a second call for the same name even when the stored value was null; previously such a parameter could be overwritten (450720cb) +- **Changed** `Nette\Bridges\ApplicationLatte\TemplateFactory` sets the default variables only when the template class declares them as properties and the value is not null (981a81d2) +- **Changed** `Nette\Application\UI\Control::createTemplate()` and `Nette\Application\UI\Presenter::createTemplate()` look up a template class by naming convention (`FooControl` to `FooTemplate`) and use it instead of the default one when it exists (6c24aaa7) +- **Changed** a flash message always expires after 30 seconds; previously after 3 seconds unless the response was a redirect (669e02c0) +- **Changed** `Nette\Application\UI\Presenter::run()` always calls `sendTemplate()` regardless of what `getTemplate()` returned; overriding getTemplate() to return null no longer suppresses rendering (74c9111c) +- **Changed** `Nette\Bridges\ApplicationLatte\Template::render()` passes the whole template object to Latte instead of an array of parameters, so the template's public methods become available as Latte functions (c3de3299) +- **Changed** `Nette\Bridges\ApplicationLatte\Template::renderToString()` passes the whole template object to Latte instead of an array of parameters (c3de3299) +- **Config** `application.catchExceptions` is forced to true in production mode regardless of the configured value (2a12b806) +- **Config** `application.scanFilter` is an fnmatch pattern and defaults to `*Presenter`; previously a substring searched in the class name with the default "Presenter" (00dc0177) +- **Config** `routing.routeClass`: deprecated, using it triggers E_USER_DEPRECATED (f8677b06) +- **Config** `routing.router`: the service is created only when at least one route is configured (aeafb720) +- **Config** `routing.router`: when no other service of type `Nette\Routing\Router` exists, a default `Nette\Application\Routers\SimpleRouter` is registered (5c2b5413) + +## 3.0.8 + +- **Renamed** `Nette\Application\UI\Form::disableSameSiteProtection()` → `Nette\Application\UI\Form::allowCrossOrigin()`; the old name is silently deprecated, also in 3.1.2 (72d5945c) + +## 3.0.7 + +- **Config** `application.debugger`: when not set explicitly, the panel is enabled by the presence of a `Tracy\BlueScreen` service; previously by whether tracy/tracy was installed (872c1434) +- **Config** `routing.debugger`: when not set explicitly, the routing panel is enabled by the presence of a `Tracy\Bar` service; previously by whether tracy/tracy was installed (872c1434) + +## 3.0.5 + +- **Changed** unserializing `Nette\Bridges\ApplicationLatte\Template` throws `Nette\NotImplementedException`; previously it succeeded silently (edcd93d6) +- **Changed** `Nette\Application\Request::isMethod()` returns false when the request method is null; previously it threw a `TypeError` under strict types (2974fbfd) + +## 3.0.4 + +- **Changed** `Nette\Application\UI\Presenter::restoreRequest()` redirects via `redirectUrl()` on a GET request; previously it always forwarded the restored request with `Nette\Application\Responses\ForwardResponse` (c84fbf32) + +## 3.0.3 + +- **Signature** `$context`, `$presenterFactory` and `$router` of `Nette\Application\UI\Presenter::injectPrimary()` lost their default null value and must be passed explicitly (edcfc7eb) +- **Changed** `Nette\Application\Responses\FileResponse::__construct()` throws `Nette\Application\BadRequestException` also when the file exists but is not readable; previously only when it did not exist (095b6f2f) +- **Changed** `Nette\Application\Responses\FileResponse::send()` throws `Nette\Application\BadRequestException` when the file cannot be opened; previously it continued with an invalid handle (095b6f2f) +- **Config** `application.scanDirs` takes priority over a globally registered RobotLoader; previously the global loader always won and a differing value was ignored (6ef62373) + +## 3.0.2.1 + +- **Changed** the `callback` route parameter of `NetteModule\MicroPresenter` must be a `\Closure`; other callables are rejected with `Nette\InvalidStateException` (f3b08453) +- **Changed** a request whose presenter name starts with `Nette:`, other than `Nette:Micro`, throws `Nette\Application\BadRequestException`; previously it was routed to that presenter (5d542d8f) + +## 3.0.2 + +- **Removed** `Nette\Bridges\ApplicationLatte\Loader` (67f45fb7) +- **Deprecated** `Nette\Application\UI\Component::hasPresenter()`: use `getPresenterIfExists() !== null`; the method is silently deprecated (56e9d459) +- **Deprecated** `Nette\Application\UI\Form::hasPresenter()`: use `getPresenterIfExists() !== null`; the method is silently deprecated (56e9d459) +- **Deprecated** `Nette\Application\UI\Presenter::hasPresenter()`: use `getPresenterIfExists() !== null`; the method is silently deprecated (56e9d459) +- **Changed** the type of a persistent parameter is taken from the declared property type when there is one; previously always from the type of its default value, so typed persistent properties are converted and validated more strictly (03e94280) +- **Changed** `Nette\Application\UI\Link::__toString()` rethrows an exception raised during the conversion on PHP 7.4 and newer; previously it was turned into a fatal E_USER_ERROR (5ae6bc13) + +## 3.0.1 + +- **Changed** `Nette\Application\Application::createInitialRequest()` throws `Nette\InvalidStateException` when the router returns parameters without a string presenter key; previously `Nette\Application\BadRequestException` (104d54e4) +- **Changed** links generated by `Nette\Application\UI\Presenter::link()` and `Nette\Application\LinkGenerator` and the baseUrl template variable are built from the URL without user info; previously a `user:pass@` part of the request was kept in them (d1378d43) + +## 3.0.0 + +- **Removed** `Nette\Application\IRouter::SECURED`: specify the protocol directly in the route mask (afd8d321) +- **Removed** `Nette\Application\Routers\Route::$defaultFlags` (afd8d321) +- **Removed** `Nette\Application\Routers\Route::MODULE_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\Route::PRESENTER_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\SimpleRouter::MODULE_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\SimpleRouter::PRESENTER_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\CliRouter::PRESENTER_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\Route::getTargetPresenters()` (77f5dfd9) +- **Removed** `Nette\Application\Routers\RouteList::warmupCache()` (77f5dfd9) +- **Renamed** `Nette\Application\UI\Control::invalidateControl()` → `Nette\Application\UI\Control::redrawControl()`; the old name was removed (afd8d321) +- **Renamed** `Nette\Bridges\ApplicationLatte\UIMacros::renderSnippets()` → `Nette\Bridges\ApplicationLatte\UIRuntime::renderSnippets()`; the old name was removed (afd8d321) +- **Renamed** the template variable `$baseUri` → `$baseUrl`; the old name was removed (bbe70369) +- **Renamed** `|url` → `|escapeUrl`; using the old name triggers E_USER_DEPRECATED (28d429e4) +- **Replaced** `Nette\Application\UI\Control::validateControl()` with `Nette\Application\UI\Control::redrawControl($snippet, false)`: the old method was removed (afd8d321) +- **Replaced** `Nette\Application\UI\Presenter::backlink()` with `Nette\Application\UI\Presenter::getAction(true)`: the old method was removed (afd8d321) +- **Replaced** `Nette\Bridges\ApplicationLatte\Template::registerHelper()` with `$template->getLatte()->addFilter()`: the old method was removed (afd8d321) +- **Replaced** calling a filter directly on the template object, i.e. `Nette\Bridges\ApplicationLatte\Template::__call()`, with `$template->getLatte()->invokeFilter($name, $args)`: the magic method was removed (afd8d321) +- **Replaced** the `$_control` template variable with the `uiControl` provider added by `$latte->addProvider()` (afd8d321) +- **Replaced** the `$_presenter` template variable with the `uiPresenter` provider added by `$latte->addProvider()` (afd8d321) +- **Replaced** `Nette\Application\UI\Component::attached()` with an override of `Nette\Application\UI\Component::validateParent()` and a `monitor()` callback: the framework no longer calls attached() (047c42f6) +- **Replaced** `Nette\Application\UI\Form::attached()` with an override of `Nette\Application\UI\Form::validateParent()` and a `monitor()` callback; chaining via `parent::attached()` no longer works either (047c42f6) +- **Replaced** the static `Nette\Application\UI\Component::getPersistentParams()` with public properties annotated `@persistent`: overriding the method has no effect, persistent parameters are discovered by reflection only (7765c92a) +- **Replaced** `Nette\Application\Application::$maxLoop` used statically with the instance property of the same name: assigning to the class no longer has any effect (8593c507) +- **Replaced** `Nette\Application\UI\Component::getParam()` with `Nette\Application\UI\Component::getParameters()` when called without an argument and with `Nette\Application\UI\Component::getParameter()` when called with one; using it triggers E_USER_DEPRECATED (800508d6) +- **Replaced** the numeric `$code` first argument of `Nette\Application\UI\Component::redirect()` with `Nette\Application\UI\Component::redirectPermanent()` for 301; passing it triggers E_USER_DEPRECATED and other codes have no replacement (29602efe) +- **Replaced** the `module` key in `$defaults` of `Nette\Application\Routers\SimpleRouter::__construct()` with `Nette\Application\Routers\RouteList::withModule()`; passing it triggers E_USER_DEPRECATED (ac454ca8) +- **Replaced** `$metadata` of `Nette\Application\Routers\Route::__construct()` passed as `Nette\Callback` with a `\Closure` (afd8d321) +- **Deprecated** `Nette\Application\IRouter`: use `Nette\Routing\Router`; the old name is silently deprecated (77f5dfd9) +- **Deprecated** `Nette\Application\Routers\Route::$styles`: using it triggers E_USER_DEPRECATED (a0474ca5) +- **Deprecated** the `@User(loggedIn)` annotation on presenter action and render methods: check the user in the action or in `Nette\Application\UI\Presenter::checkRequirements()`; using it triggers E_USER_DEPRECATED (f90e7dca) +- **Deprecated** `|normalize`: use `Nette\Utils\Strings::normalize()`; using it triggers E_USER_DEPRECATED (28d429e4) +- **Deprecated** `|toAscii`: use `Nette\Utils\Strings::toAscii()`; using it triggers E_USER_DEPRECATED (28d429e4) +- **Deprecated** `|reverse`: using it triggers E_USER_DEPRECATED (28d429e4) +- **Deprecated** `|null`: using it triggers E_USER_DEPRECATED (28d429e4) +- **Signature** native parameter and return types were added throughout the package; affects implementations and overrides of `Nette\Application\IPresenter::run()`, `Nette\Application\IPresenterFactory::createPresenter()`, `Nette\Application\IPresenterFactory::getPresenterClass()`, `Nette\Application\IResponse::send()`, `Nette\Application\UI\IRenderable::isControlInvalid()`, `Nette\Application\UI\IRenderable::redrawControl()`, `Nette\Application\UI\ISignalReceiver::signalReceived()`, `Nette\Application\UI\IStatePersistent::loadState()`, `Nette\Application\UI\IStatePersistent::saveState()`, `Nette\Application\UI\ITemplate::render()`, `Nette\Application\UI\ITemplate::setFile()`, `Nette\Application\UI\ITemplate::getFile()`, `Nette\Application\UI\ITemplateFactory::createTemplate()`, `Nette\Bridges\ApplicationLatte\ILatteFactory::create()`, `Nette\Application\UI\Component::validateParent()`, `Nette\Application\UI\Component::checkRequirements()`, `Nette\Application\UI\Control::templatePrepareFilters()`, `Nette\Application\UI\Multiplier::createComponent()`, `Nette\Application\UI\Form::isAnchored()`, `Nette\Application\UI\Form::receiveHttpData()` and `Nette\Application\UI\Presenter::shutdown()` (b71d69c5, c94f654f, 3f03051d) +- **Signature** `Nette\Application\IRouter::match()` returns an array of parameters instead of `Nette\Application\Request`; affects custom routers, the new `Nette\Application\Request::toArray()` does the opposite conversion (ed4be551) +- **Signature** `Nette\Application\IRouter::constructUrl()` takes an array of parameters instead of `Nette\Application\Request`; affects custom routers (ed4be551) +- **Signature** `$refUrl` of `Nette\Application\IRouter::constructUrl()` is `Nette\Http\UrlScript` instead of `Nette\Http\Url`; affects custom routers (a0ddbee7) +- **Signature** `$refUrl` of `Nette\Application\LinkGenerator::__construct()` is `Nette\Http\UrlScript` instead of `Nette\Http\Url` (a0ddbee7) +- **Signature** `$reflection` of `Nette\Application\UI\Component::saveState()` was removed; affects subclasses overriding it and calls passing a second argument (86101dd9) +- **Signature** `$throw` of `Nette\Application\UI\Component::getPresenter()`, `Nette\Application\UI\Form::getPresenter()` and `Nette\Application\UI\Presenter::getPresenter()` was removed from the signature; passing it triggers E_USER_DEPRECATED, use `hasPresenter()` instead of `getPresenter(false)` (31ff0947) +- **Signature** `$message` of `Nette\Application\UI\Component::error()` and `NetteModule\MicroPresenter::error()` is `string` with the default `''`; passing null throws a `TypeError` (66c40ddc) +- **Signature** `$name` of `Nette\Application\UI\Control::getSnippetId()` is required and typed `string` (b71d69c5) +- **Signature** `$destination` of `Nette\Application\UI\Presenter::redirectPermanent()` is required and typed `string` (b71d69c5) +- **Signature** `Nette\Application\Application::getRouter()` returns `Nette\Routing\Router` instead of `Nette\Application\IRouter` (fc1a9f70) +- **Signature** these classes are final and can no longer be extended: `Nette\Application\Helpers`, `Nette\Application\LinkGenerator`, `Nette\Application\Request`, `Nette\Application\Responses\CallbackResponse`, `Nette\Application\Responses\FileResponse`, `Nette\Application\Responses\ForwardResponse`, `Nette\Application\Responses\JsonResponse`, `Nette\Application\Responses\RedirectResponse`, `Nette\Application\Responses\TextResponse`, `Nette\Application\Responses\VoidResponse`, `Nette\Application\Routers\CliRouter`, `Nette\Application\Routers\SimpleRouter`, `Nette\Application\UI\Link`, `Nette\Application\UI\Multiplier`, `Nette\Bridges\ApplicationDI\ApplicationExtension`, `Nette\Bridges\ApplicationDI\LatteExtension`, `Nette\Bridges\ApplicationDI\RoutingExtension`, `Nette\Bridges\ApplicationLatte\UIMacros`, `Nette\Bridges\ApplicationTracy\RoutingPanel`, `NetteModule\ErrorPresenter` and `NetteModule\MicroPresenter` (a70c7256) +- **Signature** these methods are final and can no longer be overridden: `Nette\Application\Application::getPresenter()`, `Nette\Application\Application::getRequests()`, `Nette\Application\UI\Component::getParameter()`, `Nette\Application\UI\Component::getParameters()`, `Nette\Application\UI\Component::getParameterId()`, `Nette\Application\UI\Control::getTemplate()`, `Nette\Application\UI\Control::setTemplateFactory()`, `Nette\Application\UI\Form::getPresenter()`, `Nette\Application\UI\Presenter::createRequest()`, `Nette\Application\UI\Presenter::getAction()`, `Nette\Application\UI\Presenter::getView()`, `Nette\Application\UI\Presenter::getLayout()`, `Nette\Application\UI\Presenter::getRequest()`, `Nette\Application\UI\Presenter::getPayload()`, `Nette\Application\UI\Presenter::getSignal()`, `Nette\Application\UI\Presenter::getUser()`, `Nette\Application\UI\Presenter::getSession()`, `Nette\Application\UI\Presenter::getContext()`, `Nette\Application\UI\Presenter::getHttpRequest()`, `Nette\Application\UI\Presenter::getHttpResponse()`, `Nette\Application\UI\Presenter::getTemplateFactory()`, `Nette\Application\UI\Presenter::isSignalReceiver()`, `Nette\Application\UI\Presenter::injectPrimary()`, `Nette\Bridges\ApplicationLatte\Template::getLatte()`, `Nette\Bridges\ApplicationLatte\Template::getFile()`, `Nette\Bridges\ApplicationLatte\Template::getParameters()` and `Nette\Bridges\ApplicationLatte\Template::__toString()` (a70c7256) +- **Changed** `Nette\Application\Routers\RouteList` extends `Nette\Routing\RouteList` instead of `Nette\Utils\ArrayList`; code relying on the ArrayList behavior breaks (77f5dfd9) +- **Changed** a signal is rejected and redirected through `Nette\Application\UI\Presenter::detectedCsrf()` when the HTTP request is not same-site; annotate the handler `@crossOrigin` to keep accepting cross-site calls (87696e09) +- **Changed** `Nette\Application\UI\Form::signalReceived()` rejects the submit signal when the HTTP request is not same-site (87696e09) +- **Changed** `Nette\Application\UI\Presenter::run()` returns `Nette\Application\Responses\VoidResponse` instead of null and always performs the shutdown, flash and AJAX payload handling; previously only when `Nette\Application\AbortException` was thrown (ad41a4cd) +- **Changed** `NetteModule\MicroPresenter::run()` returns `Nette\Application\Responses\VoidResponse` instead of null when the callback produces no response (ad41a4cd) +- **Changed** `Nette\Application\UI\Presenter::createRequest()` parses the destination with a strict regular expression; a destination that does not have the shape `[//] path[!][?query][#fragment]` throws `Nette\Application\UI\InvalidLinkException` (1c581500) +- **Changed** `NetteModule\ErrorPresenter::run()` renders the error template only when the response content type is text/html; previously always (82099365) +- **Changed** `Nette\Application\Responses\FileResponse::__construct()` falls back to the file base name only when `$name` is null; previously for any falsy value (bcaa9d91) +- **Changed** `Nette\Application\Routers\Route::constructUrl()` casts all scalar parameters to strings, so false becomes `'0'`; previously only parameters with a fixity were converted and a false default in a mask became an empty string (b62d2f52) +- **Changed** `Nette\Application\Routers\RouteList::getModule()` returns null instead of an empty string when the router was created without a module (06f98aac) +- **Changed** `Nette\Application\Routers\Route::match()` always returns the matched array and no longer throws `Nette\InvalidStateException` nor returns null when the presenter parameter is missing; the validation happens in `Nette\Application\Application::createInitialRequest()` (ae6e0bc4) +- **Changed** `Nette\Application\Routers\SimpleRouter::match()` always matches and no longer returns null when the presenter parameter is missing, so inside a `Nette\Application\Routers\RouteList` it no longer defers to the next router (ae6e0bc4) +- **Changed** `Nette\Application\UI\Presenter::changeAction()` no longer validates the action name; the validation happens once during request initialization (50e49fda) +- **Changed** `Nette\Bridges\ApplicationLatte\Template::setTranslator()` called with null keeps the `|translate` filter defined and returns the text unchanged; previously the filter was removed and using it failed (fc63a718) +- **Changed** `Nette\Application\UI\Control::getTemplate()` fails with a native `TypeError` when `createTemplate()` returns something else than a template; previously `Nette\UnexpectedValueException` with a descriptive message (bf347899) +- **Changed** `Nette\Application\PresenterFactory::getPresenterClass()` fails with a native `TypeError` for a non-string name; previously `Nette\Application\InvalidPresenterException` (bf347899) +- **Changed** `NetteModule\MicroPresenter` throws `Nette\InvalidStateException` (500) when the route has no callback parameter; previously `Nette\Application\BadRequestException` (404) (b7df270f) +- **Changed** presenter services registered by `Nette\Bridges\ApplicationDI\ApplicationExtension` are not autowired; reference a presenter explicitly when another service needs it (2a8d32a4) +- **Config** the `nette.presenter` DI service tag is no longer used, presenters are found by type (cb354357) From 22928c214a7af9c0479d34e5a525bc6eea499ca6 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 27 Aug 2026 00:41:22 +0200 Subject: [PATCH 11/54] fixed PHPStan errors --- phpstan.neon | 11 ----------- src/Application/LinkGenerator.php | 8 +++----- src/Application/UI/Presenter.php | 2 +- src/Bridges/ApplicationDI/LatteExtension.php | 2 +- tests/types/application-types.php | 2 +- 5 files changed, 6 insertions(+), 19 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 89a7fdc1d..3336ce52a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -27,7 +27,6 @@ parameters: identifiers: - function.alreadyNarrowedType - identical.alwaysTrue - - booleanAnd.alwaysTrue # PresenterFactory: default factory `new $class` yields object; $class is a validated class-string at the call site - @@ -50,11 +49,6 @@ parameters: identifier: nullCoalesce.offset path: src/Application/Routers/Route.php - # parse_str() native type is wider than actual query string result - - - identifier: varTag.nativeType - path: src/Application/LinkGenerator.php - # MicroPresenter uses duck typing for template objects and nullable container - path: src/Application/MicroPresenter.php @@ -65,11 +59,6 @@ parameters: - method.nonObject - property.notFound - # ErrorPresenter: $code is used in the required template file - - - identifier: closure.unusedUse - path: src/Application/ErrorPresenter.php - # BadRequestException intentionally narrows inherited Exception::$code to int (HTTP status codes only) - identifier: property.phpDocType diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index 9b19fd372..c5f9512b0 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -251,7 +251,7 @@ public function createRequest( if (!empty($signal)) { assert($component !== null); $args[UI\Presenter::SignalKey] = $component->getParameterId($signal); - $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter?->getParameter(UI\Presenter::SignalKey); + $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter->getParameter(UI\Presenter::SignalKey); } if (($mode === 'redirect' || $mode === 'forward') && $refPresenter?->hasFlashSession()) { @@ -272,7 +272,7 @@ public function createRequest( /** * Parse destination in format "[//] [[[module:]presenter:]action | signal! | this | @alias] [?query] [#fragment]" * @throws UI\InvalidLinkException - * @return array{absolute: bool, path: string, signal: bool, args: ?array, fragment: string} + * @return array{absolute: bool, path: string, signal: bool, args: mixed[]|null, fragment: string} * @internal */ public static function parseDestination(string $destination): array @@ -287,9 +287,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), $parsed); - /** @var array $args */ - $args = $parsed; + parse_str(substr($matches['query'], 1), $args); } return [ diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 9d798b416..f0c3d8796 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -860,7 +860,7 @@ protected function createRequest(Component $component, string $destination, arra /** - * @return array{absolute: bool, path: string, signal: bool, args: ?array, fragment: string} + * @return array{absolute: bool, path: string, signal: bool, args: mixed[]|null, fragment: string} */ #[\Deprecated] public static function parseDestination(string $destination): array diff --git a/src/Bridges/ApplicationDI/LatteExtension.php b/src/Bridges/ApplicationDI/LatteExtension.php index 62c04ab7a..1709604ed 100644 --- a/src/Bridges/ApplicationDI/LatteExtension.php +++ b/src/Bridges/ApplicationDI/LatteExtension.php @@ -88,7 +88,7 @@ public function loadConfiguration(): void foreach ($config->extensions as $extension) { if ($extension === Latte\Essential\TranslatorExtension::class) { - $extension = new Statement($extension, [new Nette\DI\Definitions\Reference(Nette\Localization\Translator::class)]); + $extension = new Statement($extension, ['@' . Nette\Localization\Translator::class]); } $this->addExtension($extension); } diff --git a/tests/types/application-types.php b/tests/types/application-types.php index 363e9be0a..e4f65e228 100644 --- a/tests/types/application-types.php +++ b/tests/types/application-types.php @@ -146,7 +146,7 @@ function testHelpersGetClassesAndTraits(): void function testParseDestination(): void { assertType( - 'array{absolute: bool, path: string, signal: bool, args: array|null, fragment: string}', + 'array{absolute: bool, path: string, signal: bool, args: array|null, fragment: string}', LinkGenerator::parseDestination('Presenter:action'), ); } From 82f5bf89f336b2a3a3189b993d63de9092950d73 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 7 Sep 2026 23:06:19 +0200 Subject: [PATCH 12/54] Application: fixes the error-handling flow in run() - onShutdown was invoked with a single argument on the success path, although its type is callable(static, ?Throwable), so a handler declared according to the type crashed with ArgumentCountError. - createErrorRequest() caught only AbortException, but forward() may also throw InvalidLinkException (e.g. a typo in the errorPresenter name) or BadRequestException. Such an exception escaped run() past all the handling: onShutdown was skipped and the final rethrow never happened. --- src/Application/Application.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Application/Application.php b/src/Application/Application.php index 7a70369dd..37972cfc5 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -66,7 +66,7 @@ public function run(): void Arrays::invoke($this->onStartup, $this); $this->processRequest($this->createInitialRequest()) ->send($this->httpRequest, $this->httpResponse); - Arrays::invoke($this->onShutdown, $this); + Arrays::invoke($this->onShutdown, $this, null); } catch (\Throwable $e) { $this->sendHttpCode($e); @@ -182,6 +182,8 @@ public function createErrorRequest(\Throwable $e): ?Request $this->presenter->forward(":$errorPresenter:", $args); } catch (AbortException) { return $this->presenter->getLastCreatedRequest(); + } catch (\Throwable) { + // a misconfigured error presenter must not replace $e, it fails in a controlled way inside processRequest() } } From d75e43f4a346451741f33e83976f608231e6de48 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 7 Sep 2026 23:51:02 +0200 Subject: [PATCH 13/54] MicroPresenter: fixes uninitialized $request and a missing container check getRequest() called before run() failed with 'must not be accessed before initialization' instead of returning null, and createTemplate() without $latteFactory dereferenced a null container. --- src/Application/MicroPresenter.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Application/MicroPresenter.php b/src/Application/MicroPresenter.php index bd0a019c9..b253757c9 100644 --- a/src/Application/MicroPresenter.php +++ b/src/Application/MicroPresenter.php @@ -20,7 +20,7 @@ */ final class MicroPresenter implements Application\IPresenter { - private ?Application\Request $request; + private ?Application\Request $request = null; public function __construct( @@ -109,7 +109,9 @@ public function createTemplate(?string $class = null, ?callable $latteFactory = { $latte = $latteFactory ? $latteFactory() - : $this->context->getByType(Nette\Bridges\ApplicationLatte\LatteFactory::class)->create(); + : ($this->context ?? throw new Nette\InvalidStateException('Unable to create template, provide the $latteFactory parameter or DI container.')) + ->getByType(Nette\Bridges\ApplicationLatte\LatteFactory::class) + ->create(); $template = $class ? new $class : new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte); From fbc52a36c615b3d030e11b4f176efb857875908b Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 8 Sep 2026 00:38:46 +0200 Subject: [PATCH 14/54] Routers\RouteList: constructUrl() exempts Nette: presenters from the module prefix completeParameters() deliberately skips module prefixing for Nette:* presenters, but constructUrl() had no mirror exemption and returned null. A link to a micro route inside withModule() could not be built at all and the canonicalization of MicroPresenter silently never happened there. --- docs/internals/routing.md | 7 ++++--- src/Application/Routers/RouteList.php | 7 ++++--- tests/Routers/RouteList.withModule.phpt | 12 ++++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/internals/routing.md b/docs/internals/routing.md index 117e22321..a5ed3868d 100644 --- a/docs/internals/routing.md +++ b/docs/internals/routing.md @@ -26,7 +26,7 @@ The Application `RouteList` adds module scoping on match: `completeParameters` **prepends the list's `module` to the matched presenter — unless it starts with `Nette:`**. `constructUrl` is the mirror: with a `module` set, the target presenter must start with that prefix (else `null` — not this list's route), and the prefix -is stripped before delegating down. `withModule($m)` creates a nested list whose +is stripped before delegating down; a `Nette:` presenter is passed down unchanged. `withModule($m)` creates a nested list whose `module` is `$m.':'` (a whole group under a module), which is distinct from the per-route `` mask parameter above. `warmupCache` is *invoked* from `RoutingExtension::afterCompile` (only when `routing: cache`), but is defined in the base package. @@ -38,8 +38,9 @@ presenter except `Nette:Micro`**: `str_starts_with($presenter, 'Nette:') && $presenter !== 'Nette:Micro'` → `BadRequestException`. Framework-internal presenters must not be directly reachable from the network, but `Nette:Micro` is the legitimate target that closure routes produce (mapped to `NetteModule\MicroPresenter`), so it -is the single allowed exception. `completeParameters`' `Nette:`-prefix skip exists -for the same reason — module scoping must not be applied to it. +is the single allowed exception. The `Nette:`-prefix skip in `completeParameters` +and `constructUrl` exists for the same reason — module scoping must not be applied +to it. ## Request loop and error routing diff --git a/src/Application/Routers/RouteList.php b/src/Application/Routers/RouteList.php index c3311723e..3349bca3c 100644 --- a/src/Application/Routers/RouteList.php +++ b/src/Application/Routers/RouteList.php @@ -52,12 +52,13 @@ protected function completeParameters(array $params): ?array */ public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string { - if ($this->module) { - if (!str_starts_with($params[self::PresenterKey], $this->module)) { + $presenter = $params[self::PresenterKey] ?? null; + if ($this->module && is_string($presenter) && strncmp($presenter, 'Nette:', 6)) { // mirrors completeParameters() + if (!str_starts_with($presenter, $this->module)) { return null; } - $params[self::PresenterKey] = substr($params[self::PresenterKey], strlen($this->module)); + $params[self::PresenterKey] = substr($presenter, strlen($this->module)); } return parent::constructUrl($params, $refUrl); diff --git a/tests/Routers/RouteList.withModule.phpt b/tests/Routers/RouteList.withModule.phpt index 1c872fcdb..6dbc75041 100644 --- a/tests/Routers/RouteList.withModule.phpt +++ b/tests/Routers/RouteList.withModule.phpt @@ -1,6 +1,7 @@ withModule('Admin') + ->addRoute('micro', fn() => new Nette\Application\Responses\VoidResponse); + +$url = new Nette\Http\UrlScript('http://example.com/micro', '/'); +$params = $list->match(new Nette\Http\Request($url)); +Assert::same('Nette:Micro', $params['presenter']); +Assert::same('http://example.com/micro', $list->constructUrl($params, $url)); From cb70e4899af8f8323ddff44e5bf3d538a6a63f97 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 8 Sep 2026 01:15:13 +0200 Subject: [PATCH 15/54] PresenterFactoryCallback: fixes an endless refresh loop on ambiguous services When findByType() returned several services and none of them had the exact type (only subclasses), the filter produced an empty array, both count() branches were skipped and the code fell through to the touchToRefresh branch. Touching the file cannot help there, so the page kept refreshing forever. Such a case now throws InvalidPresenterException right away. The Refresh header is also sent before the output, otherwise it is lost with output buffering turned off. --- src/Bridges/ApplicationDI/PresenterFactoryCallback.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Bridges/ApplicationDI/PresenterFactoryCallback.php b/src/Bridges/ApplicationDI/PresenterFactoryCallback.php index fa5df5975..5dd920c98 100644 --- a/src/Bridges/ApplicationDI/PresenterFactoryCallback.php +++ b/src/Bridges/ApplicationDI/PresenterFactoryCallback.php @@ -29,10 +29,12 @@ public function __invoke(string $class): Nette\Application\IPresenter { $services = $this->container->findByType($class); if (count($services) > 1) { - $services = array_values(array_filter($services, fn($service) => $this->container->getServiceType($service) === $class)); - if (count($services) > 1) { + $exact = array_values(array_filter($services, fn($service) => $this->container->getServiceType($service) === $class)); + if (count($exact) !== 1) { throw new Nette\Application\InvalidPresenterException("Multiple services of type $class found: " . implode(', ', $services) . '.'); } + + $services = $exact; } if (count($services) === 1) { @@ -43,10 +45,10 @@ public function __invoke(string $class): Nette\Application\IPresenter if ($this->touchToRefresh && class_exists($class)) { touch($this->touchToRefresh); + header('Refresh: 5'); 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; } From e45b329c8fdbf90380f5893fb2dab7a15ab990d4 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 8 Sep 2026 01:49:30 +0200 Subject: [PATCH 16/54] tests: the robotLoader discovery test really tests its own container The test compiled Container4 but ran the assertions against Container2 from the previous test, so the findPresenters() branch using the injected RobotLoader was not covered at all. It also needs scanFilter, as the fixture classes do not match the default '*Presenter' mask. --- tests/Bridges.DI/ApplicationExtension.scan.phpt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/Bridges.DI/ApplicationExtension.scan.phpt b/tests/Bridges.DI/ApplicationExtension.scan.phpt index cc1b473a9..7bf2ace28 100644 --- a/tests/Bridges.DI/ApplicationExtension.scan.phpt +++ b/tests/Bridges.DI/ApplicationExtension.scan.phpt @@ -94,10 +94,14 @@ test('robotLoader discovery', function () { $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); $builder->addDefinition('myHttpRequest')->setCreator(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); - $code = $compiler->setClassName('Container4')->compile(); + $code = $compiler->addConfig([ + 'application' => [ + 'scanFilter' => '*Presenter*', + ], + ])->setClassName('Container4')->compile(); eval($code); - $container = new Container2; + $container = new Container4; Assert::count(3, $container->findByType(BasePresenter::class)); Assert::count(1, $container->findByType(Presenter1::class)); Assert::count(1, $container->findByType(Presenter2::class)); From 2eb3e82f6c69f514b94e41b8b7c2a4bdbb57fc8d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 16 Sep 2026 13:22:51 +0200 Subject: [PATCH 17/54] FileResponse: sanitizes filename to prevent Content-Disposition injection Strips quotes, backslashes and control characters from the filename before placing it into the quoted Content-Disposition value, so a crafted name cannot inject additional header parameters. --- src/Application/Responses/FileResponse.php | 3 ++- .../FileResponse.contentDisposition.phpt | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Application/Responses/FileResponse.php b/src/Application/Responses/FileResponse.php index 95bd1b22f..1bd07f83f 100644 --- a/src/Application/Responses/FileResponse.php +++ b/src/Application/Responses/FileResponse.php @@ -67,10 +67,11 @@ public function getContentType(): string public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void { $httpResponse->setContentType($this->contentType); + $name = str_replace(["\r", "\n", "\0", '"', '\\'], '', $this->name); $httpResponse->setHeader( 'Content-Disposition', ($this->forceDownload ? 'attachment' : 'inline') - . '; filename="' . $this->name . '"' + . '; filename="' . $name . '"' . '; filename*=utf-8\'\'' . rawurlencode($this->name), ); diff --git a/tests/Responses/FileResponse.contentDisposition.phpt b/tests/Responses/FileResponse.contentDisposition.phpt index 306f37933..3f422cdc2 100644 --- a/tests/Responses/FileResponse.contentDisposition.phpt +++ b/tests/Responses/FileResponse.contentDisposition.phpt @@ -68,3 +68,18 @@ test('filename with special characters', function () { $response->getHeader('Content-Disposition'), ); }); + + +test('filename injection into Content-Disposition is neutralized', function () { + $file = __FILE__; + $fileName = 'evil".pdf; filename="hack.exe'; + $fileResponse = new FileResponse($file, $fileName); + + ob_start(); + $fileResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); + ob_get_clean(); + + // quotes and backslashes are stripped from the quoted filename, so the header cannot be broken apart + $header = $response->getHeader('Content-Disposition'); + Assert::same('attachment; filename="evil.pdf; filename=hack.exe"; filename*=utf-8\'\'' . rawurlencode($fileName), $header); +}); From b9cb17b85635d71b8e4698335ad8ff1f4a18f9fe Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 16 Sep 2026 14:09:27 +0200 Subject: [PATCH 18/54] FileResponse: fixes Content-Range handling per RFC 9110 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Content-Range was sent even with a plain 200 response, which RFC 9110 §14.4 forbids; for an empty file it produced a malformed 'bytes 0--1/0'. - A 416 response now carries 'Content-Range: bytes */size' so the client learns the actual file size. --- src/Application/Responses/FileResponse.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Application/Responses/FileResponse.php b/src/Application/Responses/FileResponse.php index 1bd07f83f..47f986537 100644 --- a/src/Application/Responses/FileResponse.php +++ b/src/Application/Responses/FileResponse.php @@ -98,6 +98,7 @@ public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $htt if ($end < $start) { $httpResponse->setCode(416); // requested range not satisfiable + $httpResponse->setHeader('Content-Range', 'bytes */' . $filesize); return; } @@ -105,9 +106,6 @@ public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $htt $httpResponse->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $filesize); $length = $end - $start + 1; fseek($handle, $start); - - } else { - $httpResponse->setHeader('Content-Range', 'bytes 0-' . ($filesize - 1) . '/' . $filesize); } } From dcbc7b2a6349a23e3aea31372f2e9ec132054daf Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 22 Sep 2026 15:48:05 +0200 Subject: [PATCH 19/54] AccessPolicy: resolves presenter lazily The presenter was required even when the element carried no #[Requires] attribute at all, so createComponent() on a component not attached to a presenter threw InvalidStateException. It is now needed only when there is actually a rule to check. --- src/Application/UI/AccessPolicy.php | 7 +++- tests/UI/AccessPolicy.withoutPresenter.phpt | 46 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/UI/AccessPolicy.withoutPresenter.phpt diff --git a/src/Application/UI/AccessPolicy.php b/src/Application/UI/AccessPolicy.php index ab516d03f..66c8efecb 100644 --- a/src/Application/UI/AccessPolicy.php +++ b/src/Application/UI/AccessPolicy.php @@ -30,11 +30,14 @@ public function __construct( public function checkAccess(Component $component): void { + $attrs = self::applyInternalRules($this->getAttributes(), $component); + if (!$attrs) { + return; + } + $this->presenter ??= $component->getPresenter(throw: false) ?? throw new Nette\InvalidStateException('Presenter is required for checking requirements of ' . Reflection::toString($this->element)); - $attrs = $this->getAttributes(); - $attrs = self::applyInternalRules($attrs, $component); foreach ($attrs as $attribute) { $this->checkAttribute($attribute); } diff --git a/tests/UI/AccessPolicy.withoutPresenter.phpt b/tests/UI/AccessPolicy.withoutPresenter.phpt new file mode 100644 index 000000000..b4f9ba9bd --- /dev/null +++ b/tests/UI/AccessPolicy.withoutPresenter.phpt @@ -0,0 +1,46 @@ + $control['guarded'], + Nette\InvalidStateException::class, + 'Presenter is required for checking requirements of StandaloneControl::createComponentGuarded()', + ); +}); From 98dda1e414e8928e161361e1cba9ec001343f122 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 22 Sep 2026 17:36:40 +0200 Subject: [PATCH 20/54] Presenter: #[Requires(methods:)] on an action or render method can widen $allowedMethods The deprecated checkHttpMethod() ran before the method-level attributes were evaluated and its bypass was applied only to class-level #[Requires(methods:)]. An attribute on the action or render method could therefore only narrow the default whitelist, never widen it, although the deprecation notice of $allowedMethods presents the attribute as its full replacement. --- docs/internals/access-control.md | 4 ++- src/Application/UI/Presenter.php | 20 ++++++++++++-- tests/UI/Requires.methods.phpt | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/docs/internals/access-control.md b/docs/internals/access-control.md index 47235eb6f..c0125bbe6 100644 --- a/docs/internals/access-control.md +++ b/docs/internals/access-control.md @@ -24,7 +24,9 @@ site runs only the attribute policy. - `methods:` — allowed HTTP methods; `['*']` is a wildcard. **Trap:** a *class-level* `methods:` empties `$presenter->allowedMethods` to bypass the deprecated legacy `Presenter::checkHttpMethod()`, so the attribute fully replaces - it. Violation → 405 with an `Allow` header. + it. The legacy check runs before the method-level attributes, so it skips itself + when the `action*` or `render*` method of the current action carries `methods:`. + Violation → 405 with an `Allow` header. - `actions:` — method-level use is allowed **only in presenters** (`LogicException` otherwise). Violation → 4xx via `error()`. - `forward: true` — the element is reachable only via forward; it also makes the diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index f0c3d8796..22289a84c 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -338,9 +338,23 @@ public function detectedCsrf(): void /** @deprecated use #[Requires(methods: ...)] to specify allowed methods */ protected function checkHttpMethod(): void { - if ($this->allowedMethods - && !in_array($method = $this->httpRequest->getMethod(), $this->allowedMethods, true) - ) { + if (!$this->allowedMethods) { + return; + } + + $rc = static::getReflection(); + foreach ([static::formatActionMethod($this->action), static::formatRenderMethod($this->action)] as $method) { + if ($rc->hasCallableMethod($method) + && Arrays::some( + $rc->getMethod($method)->getAttributes(Application\Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF), + fn($ra) => $ra->newInstance()->methods !== null, + ) + ) { + return; // #[Requires(methods:)] on the action or render method takes precedence over $allowedMethods + } + } + + if (!in_array($method = $this->httpRequest->getMethod(), $this->allowedMethods, true)) { $this->httpResponse->setHeader('Allow', implode(',', $this->allowedMethods)); $this->error("Method $method is not allowed", Nette\Http\IResponse::S405_MethodNotAllowed); } diff --git a/tests/UI/Requires.methods.phpt b/tests/UI/Requires.methods.phpt index c32ae5e03..36a5dd191 100644 --- a/tests/UI/Requires.methods.phpt +++ b/tests/UI/Requires.methods.phpt @@ -32,6 +32,31 @@ class TestAllMethodsPresenter extends Nette\Application\UI\Presenter } +class TestActionMethodsPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: ['OPTIONS'])] + public function actionDefault(): never + { + $this->terminate(); + } +} + + +class TestRenderMethodsPresenter extends Nette\Application\UI\Presenter +{ + public function actionDefault(): void + { + } + + + #[Requires(methods: ['OPTIONS'])] + public function renderDefault(): never + { + $this->terminate(); + } +} + + $presenter = createPresenter(TestMethodsPresenter::class, method: 'OPTIONS'); Assert::noError( fn() => $presenter->run(new Application\Request('', Http\Request::Get)), @@ -50,3 +75,25 @@ $presenter = createPresenter(TestAllMethodsPresenter::class, method: 'OPTIONS'); Assert::noError( fn() => $presenter->run(new Application\Request('', Http\Request::Get)), ); + + +// attribute on the action method can allow methods outside the default whitelist +$presenter = createPresenter(TestActionMethodsPresenter::class, method: 'OPTIONS'); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), +); + + +$presenter = createPresenter(TestActionMethodsPresenter::class); +Assert::exception( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), + Application\BadRequestException::class, + 'Method GET is not allowed by TestActionMethodsPresenter::actionDefault()', +); + + +// attribute on the render method counts as well +$presenter = createPresenter(TestRenderMethodsPresenter::class, method: 'OPTIONS'); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), +); From 249dbe1080871195fee681c58c4b4f39c9ab56ab Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 12 Jul 2026 19:54:23 +0200 Subject: [PATCH 21/54] Presenter: detects an infinite loop of switch() calls Two actions switching to each other (actionA() switches to 'b', actionB() switches back to 'a') looped forever. As the target of switch() may depend on request parameters, this was remotely triggerable. --- docs/internals/lifecycle.md | 4 +++- src/Application/UI/Presenter.php | 11 ++++++++++ tests/UI/Presenter.switch.loop.phpt | 34 +++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/UI/Presenter.switch.loop.phpt diff --git a/docs/internals/lifecycle.md b/docs/internals/lifecycle.md index 33f487ec9..d1dd98771 100644 --- a/docs/internals/lifecycle.md +++ b/docs/internals/lifecycle.md @@ -28,7 +28,9 @@ beforeRender → onRender → render → afterRender → sendTemplate**. A `SwitchException` escaping anywhere other than the two retry sites above is turned by `run()` into a `LogicException` ("Switch is only allowed inside -action*() or render*() method"). +action*() or render*() method"). The two retry sites share one counter; more than +`Presenter::MaxSwitches` switches in a single `run()` end with a `LogicException` +too, so actions switching to each other cannot loop forever. ## Termination is by exception; there is no explicit "frozen" flag diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 22289a84c..3ac828e13 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -69,6 +69,8 @@ abstract class Presenter extends Control implements Application\IPresenter /** @deprecated use Presenter::DefaultAction */ public const DEFAULT_ACTION = self::DefaultAction; + private const MaxSwitches = 20; + public int $invalidLinkMode = self::InvalidLinkSilent; /** @var array Occurs before starup() */ @@ -210,10 +212,15 @@ public function run(Application\Request $request): Application\Response } // calls $this->action() + $switched = 0; try { actionMethod: $this->tryCall(static::formatActionMethod($this->action), $this->params); } catch (Application\SwitchException $e) { + if (++$switched > self::MaxSwitches) { + throw new \LogicException('Infinite loop of switch() calls detected.', 0, $e); + } + $this->changeAction($e->getMessage()); $this->autoCanonicalize = false; goto actionMethod; @@ -244,6 +251,10 @@ public function run(Application\Request $request): Application\Response renderMethod: $this->tryCall(static::formatRenderMethod($this->view), $this->params); } catch (Application\SwitchException $e) { + if (++$switched > self::MaxSwitches) { + throw new \LogicException('Infinite loop of switch() calls detected.', 0, $e); + } + $this->setView($e->getMessage()); goto renderMethod; } diff --git a/tests/UI/Presenter.switch.loop.phpt b/tests/UI/Presenter.switch.loop.phpt new file mode 100644 index 000000000..f0ee94304 --- /dev/null +++ b/tests/UI/Presenter.switch.loop.phpt @@ -0,0 +1,34 @@ +switch('bar'); + } + + + public function actionBar(): void + { + $this->switch('foo'); + } +} + + +$presenter = createPresenter(SwitchLoopPresenter::class); +Assert::exception( + fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD, ['action' => 'foo'])), + LogicException::class, + 'Infinite loop of switch() calls detected.', +); From 20e0725f50aa1cefa1ec745ace106f95f2554203 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 2 Oct 2026 23:41:58 +0200 Subject: [PATCH 22/54] ComponentReflection: getActionRenderMethod() caches negative results too The ??= operator never caches a null result, so for actions without an action*() or render*() method the whole reflection lookup was repeated for every generated link. --- src/Application/UI/ComponentReflection.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Application/UI/ComponentReflection.php b/src/Application/UI/ComponentReflection.php index 4e7012233..6a6029f3a 100644 --- a/src/Application/UI/ComponentReflection.php +++ b/src/Application/UI/ComponentReflection.php @@ -156,11 +156,15 @@ public function hasCallableMethod(string $method): bool public function getActionRenderMethod(string $action): ?\ReflectionMethod { $class = $this->name; - return self::$armCache[$class][$action] ??= - $this->hasCallableMethod($name = $class::formatActionMethod($action)) - || $this->hasCallableMethod($name = $class::formatRenderMethod($action)) - ? parent::getMethod($name) - : null; + if (!array_key_exists($action, self::$armCache[$class] ?? [])) { + self::$armCache[$class][$action] = + $this->hasCallableMethod($name = $class::formatActionMethod($action)) + || $this->hasCallableMethod($name = $class::formatRenderMethod($action)) + ? parent::getMethod($name) + : null; + } + + return self::$armCache[$class][$action]; } From 11d11c38c22d3f9fe0bab251a380458f2080ca05 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 12 Oct 2026 00:28:16 +0200 Subject: [PATCH 23/54] UIExtension: findNonce() accepts a base64url nonce The character class did not include '-', so a base64url nonce (emitted by common CSP middlewares) was matched truncated and n:nonce then rendered a wrong value, silently blocking the scripts. A nonce at the very beginning of the header was not matched at all. --- src/Bridges/ApplicationLatte/UIExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/ApplicationLatte/UIExtension.php b/src/Bridges/ApplicationLatte/UIExtension.php index da10e9412..00dc09b7f 100644 --- a/src/Bridges/ApplicationLatte/UIExtension.php +++ b/src/Bridges/ApplicationLatte/UIExtension.php @@ -123,7 +123,7 @@ private function findNonce(Nette\Http\IResponse $httpResponse): ?string { $header = $httpResponse->getHeader('Content-Security-Policy') ?: $httpResponse->getHeader('Content-Security-Policy-Report-Only'); - return preg_match('#\s\'nonce-([\w+/]+=*)\'#', (string) $header, $m) ? $m[1] : null; + return preg_match('#(?:^|\s)\'nonce-([\w+/-]+=*)\'#', (string) $header, $m) ? $m[1] : null; } From a02bab424f261f55cf168276fb4d3ea3355c8700 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 12 Oct 2026 00:57:39 +0200 Subject: [PATCH 24/54] UIExtension: adds getCacheKey() depending on the snippet attribute SnippetNode::$snippetAttribute affects the generated code, but the extension did not report it in the cache key, so changing it left stale compiled templates with the original attribute. --- src/Bridges/ApplicationLatte/UIExtension.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Bridges/ApplicationLatte/UIExtension.php b/src/Bridges/ApplicationLatte/UIExtension.php index 00dc09b7f..ed2310cfc 100644 --- a/src/Bridges/ApplicationLatte/UIExtension.php +++ b/src/Bridges/ApplicationLatte/UIExtension.php @@ -97,6 +97,12 @@ public function getPasses(): array } + public function getCacheKey(Latte\Engine $engine): mixed + { + return ['snippetAttribute' => Nodes\SnippetNode::$snippetAttribute]; + } + + /** * Render snippets instead of template in snippet-mode. */ From 0127807f9784629f2704ba6927e765d9a38c1ff5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 19 Oct 2026 13:44:22 +0200 Subject: [PATCH 25/54] RoutingPanel: fixes lost {else} branch and missing space in tab The conversion of the panel templates to Latte turned a ternary into an {if} without the {else} branch, so non-string defaults and parameters were not rendered at all, while string ones were rendered twice (escaped text plus the full dump). The tab lost the space before the signal name. --- src/Bridges/ApplicationTracy/dist/panel.phtml | 6 ++++-- src/Bridges/ApplicationTracy/dist/tab.phtml | 3 ++- src/Bridges/ApplicationTracy/panel.latte | 4 ++-- src/Bridges/ApplicationTracy/tab.latte | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Bridges/ApplicationTracy/dist/panel.phtml b/src/Bridges/ApplicationTracy/dist/panel.phtml index 724c4db46..856b1e0a3 100644 --- a/src/Bridges/ApplicationTracy/dist/panel.phtml +++ b/src/Bridges/ApplicationTracy/dist/panel.phtml @@ -225,7 +225,8 @@ if (empty($routes['routes'])) /* pos 111:2 */ { if (is_string($value)) /* pos 155:28 */ { echo Tracy\Helpers::escapeHtml($value) /* pos 155:50 */; echo '
'; - echo Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) /* pos 155:62 */; + } else /* pos 155:62 */ { + echo Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) /* pos 155:68 */; } echo "\n"; @@ -257,7 +258,8 @@ if (empty($routes['routes'])) /* pos 111:2 */ { if (is_string($value)) /* pos 170:29 */ { echo Tracy\Helpers::escapeHtml($value) /* pos 170:51 */; echo '
'; - echo Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) /* pos 170:63 */; + } else /* pos 170:63 */ { + echo Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) /* pos 170:69 */; } echo "\n"; diff --git a/src/Bridges/ApplicationTracy/dist/tab.phtml b/src/Bridges/ApplicationTracy/dist/tab.phtml index 7f66097f5..75934178a 100644 --- a/src/Bridges/ApplicationTracy/dist/tab.phtml +++ b/src/Bridges/ApplicationTracy/dist/tab.phtml @@ -18,7 +18,8 @@ if ($matched === null) /* pos 10:3 */ { echo ':'; echo Tracy\Helpers::escapeHtml($matched[Presenter::ActionKey] ?? Presenter::DefaultAction) /* pos 13:40 */; if (isset($matched[Presenter::SignalKey])) /* pos 13:100 */ { - echo Tracy\Helpers::escapeHtml($matched[Presenter::SignalKey]) /* pos 13:142 */; + echo ' '; + echo Tracy\Helpers::escapeHtml($matched[Presenter::SignalKey]) /* pos 13:143 */; echo '!'; } echo "\n"; diff --git a/src/Bridges/ApplicationTracy/panel.latte b/src/Bridges/ApplicationTracy/panel.latte index 972bfd3f8..080482c1c 100644 --- a/src/Bridges/ApplicationTracy/panel.latte +++ b/src/Bridges/ApplicationTracy/panel.latte @@ -152,7 +152,7 @@
{foreach $route->defaults as $key => $value} - {$key} = {if is_string($value)}{$value}
{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} + {$key} = {if is_string($value)}{$value}
{else}{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} {/foreach}
@@ -167,7 +167,7 @@ {do $params = array_diff_key($params, [Presenter::PresenterKey => 1, Presenter::ActionKey => 1])} {/if} {foreach $params as $key => $value} - {$key} = {if is_string($value)}{$value}
{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} + {$key} = {if is_string($value)}{$value}
{else}{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} {/foreach} {elseif $route->error} diff --git a/src/Bridges/ApplicationTracy/tab.latte b/src/Bridges/ApplicationTracy/tab.latte index 7fa668ecd..1c3e41b6d 100644 --- a/src/Bridges/ApplicationTracy/tab.latte +++ b/src/Bridges/ApplicationTracy/tab.latte @@ -10,7 +10,7 @@ {if $matched === null} no route {elseif isset($matched[Presenter::PresenterKey])} - {$matched[Presenter::PresenterKey]}:{$matched[Presenter::ActionKey] ?? Presenter::DefaultAction}{if isset($matched[Presenter::SignalKey])}{$matched[Presenter::SignalKey]}!{/if} + {$matched[Presenter::PresenterKey]}:{$matched[Presenter::ActionKey] ?? Presenter::DefaultAction}{if isset($matched[Presenter::SignalKey])} {$matched[Presenter::SignalKey]}!{/if} {/if} From f59fd93bcd2f22122b98277eaa227415b8adf7a6 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 18:39:44 +0100 Subject: [PATCH 26/54] LinkGenerator: isLinkCurrent() does not throw on targets with #[Requires(forward: true)] --- docs/internals/link-generation.md | 2 +- src/Application/LinkGenerator.php | 2 +- tests/UI/Requires.forward.link().phpt | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/internals/link-generation.md b/docs/internals/link-generation.md index 02d4a61b2..3ada2cd36 100644 --- a/docs/internals/link-generation.md +++ b/docs/internals/link-generation.md @@ -15,7 +15,7 @@ The `$mode` string is **not** just `link`/`redirect`/`forward`. There are **five | `link` | relative (unless `absoluteUrls` or `//` prefix) | no | the normal case | | `redirect` | absolute | yes* | `Component::redirect` | | `forward` | — (returns null) | yes* | Request via `$lastRequest`; no `isLinkable` enforcement | -| `test` | — (returns null) | no | `isLinkCurrent`; skips the missing-param check | +| `test` | — (returns null) | no | `isLinkCurrent`; skips the missing-param check and `isLinkable` enforcement | | `redirectX` | absolute | no | **canonicalization only** | `redirectX` is **not a phantom** — it is live in `Presenter::canonicalize()`. It diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index c5f9512b0..33cd24244 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -345,7 +345,7 @@ private function validateLinkTarget( $message .= $presenter === null ? '' : " from '{$presenter->getName()}:{$presenter->getAction()}'"; - if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->isLinkable()) { + if ($mode !== 'forward' && $mode !== 'test' && !(new UI\AccessPolicy($element))->isLinkable()) { throw new UI\InvalidLinkException("Link to forbidden $message."); } elseif ($presenter?->invalidLinkMode && (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class)) // dresscode:ignore upgrading.declarations.deprecatedMember -- @deprecated is still supported diff --git a/tests/UI/Requires.forward.link().phpt b/tests/UI/Requires.forward.link().phpt index 49700e144..0c4221284 100644 --- a/tests/UI/Requires.forward.link().phpt +++ b/tests/UI/Requires.forward.link().phpt @@ -90,7 +90,24 @@ Assert::error( Assert::noError(fn() => $presenter->link('Test:')); +Assert::false($presenter->isLinkCurrent('foo')); +Assert::false($presenter->isLinkCurrent('Forward:')); +Assert::false($presenter->isLinkCurrent('Forward:*')); + Assert::error( // no error fn() => $presenter->forward('Forward:'), Application\AbortException::class, ); + + +$presenter = new ForwardPresenter; +$presenter->injectPrimary( + new Http\Request($url), + new Http\Response, + $presenterFactory, + new Application\Routers\SimpleRouter, +); +$presenter->run(new Application\Request('Forward', Application\Request::Forward)); + +Assert::true($presenter->isLinkCurrent('this')); +Assert::true($presenter->isLinkCurrent('Forward:*')); From a8ff722c5ca984815a48c3c6466fa32b50c59839 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 18:40:20 +0100 Subject: [PATCH 27/54] TemplateFactory: removed redundant class_exists() --- src/Bridges/ApplicationLatte/TemplateFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/ApplicationLatte/TemplateFactory.php b/src/Bridges/ApplicationLatte/TemplateFactory.php index bfc843fd6..4f51f3fb1 100644 --- a/src/Bridges/ApplicationLatte/TemplateFactory.php +++ b/src/Bridges/ApplicationLatte/TemplateFactory.php @@ -28,7 +28,7 @@ public function __construct( private readonly ?Nette\Security\User $user = null, $templateClass = null, ) { - if ($templateClass && (!class_exists($templateClass) || !is_a($templateClass, Template::class, allow_string: true))) { + if ($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.'); } From 5193e2030d28989de09dfe787b99cc56d3b1acc2 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 18:43:26 +0100 Subject: [PATCH 28/54] Component: missing #[Parameter] without default value causes 4xx, a nullable one becomes null Previously the property stayed uninitialized and the first read failed with 500. --- docs/internals/persistent-state.md | 4 ++- src/Application/UI/Component.php | 12 ++++++- tests/UI/Component.loadState().phpt | 56 +++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/UI/Component.loadState().phpt diff --git a/docs/internals/persistent-state.md b/docs/internals/persistent-state.md index a3687d12b..7b3181815 100644 --- a/docs/internals/persistent-state.md +++ b/docs/internals/persistent-state.md @@ -22,7 +22,9 @@ presenter — a `since` (the declaring class, used to scope which links may carr `ComponentReflection::getParameters()` returns both `#[Persistent]` and `#[Parameter]` properties, so `loadState()` fills and type-validates **both** the -same way (a bad value → `error()` → 404). The asymmetry is on the way out: only +same way (a bad value → `error()` → 404). A missing `#[Parameter]` whose property +is still uninitialized is required: non-nullable → `error()` → 404, nullable → +`null`; a missing `#[Persistent]` leaves its property untouched. The asymmetry is on the way out: only persistent entries carry a `since` key, and `getPersistentParams()` filters on its presence — a `#[Parameter]` property is never written back by `saveState` and never reaches a URL. diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index e67ba2ffc..49c225ab7 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -166,8 +166,18 @@ public function loadState(array $params): void $this->$name = $params[$name]; - } else { + } elseif (isset($this->$name) || array_key_exists('since', $meta)) { $params[$name] = $this->$name ?? null; + + } elseif ((new \ReflectionProperty($this, $name))->getType()?->allowsNull() ?? true) { + $params[$name] = $this->$name = null; + + } else { + $this->error(sprintf( + "Missing parameter '%s' required by %s.", + $name, + $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'", + )); } } diff --git a/tests/UI/Component.loadState().phpt b/tests/UI/Component.loadState().phpt new file mode 100644 index 000000000..b4c3b41d8 --- /dev/null +++ b/tests/UI/Component.loadState().phpt @@ -0,0 +1,56 @@ + $presenter->loadState([]), + Application\BadRequestException::class, + "Missing parameter 'required' required by presenter .", + ); +}); + + +test('passed parameters', function () { + $presenter = new TestPresenter; + $presenter->loadState(['required' => 'abc']); + Assert::same('abc', $presenter->required); + Assert::null($presenter->nullable); + Assert::same('def', $presenter->default); + Assert::false(isset($presenter->persistent)); + Assert::same(['required' => 'abc', 'nullable' => null, 'default' => 'def', 'persistent' => null], $presenter->getParameters()); +}); + + +test('initialized property', function () { + $presenter = new TestPresenter; + $presenter->required = 'xyz'; + $presenter->loadState([]); + Assert::same('xyz', $presenter->required); +}); From b8a9ede0259b4904006ab430ad4be9fe1cca3238 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 18:45:07 +0100 Subject: [PATCH 29/54] ApplicationExtension: warns when a presenter overrides createRequest() or requestToUrl() Since link generation moved to LinkGenerator, these methods are not called, so the override was silently ignored. --- .../ApplicationDI/ApplicationExtension.php | 11 +++++ .../ApplicationExtension.checkPresenter.phpt | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/Bridges.DI/ApplicationExtension.checkPresenter.phpt diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php index 90f808b72..fc55c7dbc 100644 --- a/src/Bridges/ApplicationDI/ApplicationExtension.php +++ b/src/Bridges/ApplicationDI/ApplicationExtension.php @@ -300,6 +300,17 @@ private function checkPresenter(string $class): void } } + foreach (['createRequest', 'requestToUrl'] as $method) { + $rm = $rc->getMethod($method); + if ($rm->getDeclaringClass()->getName() === $class) { + trigger_error(sprintf( + 'Method %s is no longer called, overriding it has no effect; links are generated by %s.', + Reflection::toString($rm), + Nette\Application\LinkGenerator::class, + ), E_USER_WARNING); + } + } + $re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.'; foreach ($rc->getMethods() as $rm) { if (preg_match("#^(?!handleInvalidLink)($re)#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) { diff --git a/tests/Bridges.DI/ApplicationExtension.checkPresenter.phpt b/tests/Bridges.DI/ApplicationExtension.checkPresenter.phpt new file mode 100644 index 000000000..de3ebc2e5 --- /dev/null +++ b/tests/Bridges.DI/ApplicationExtension.checkPresenter.phpt @@ -0,0 +1,40 @@ + $this->checkPresenter($class))->call(new ApplicationExtension); +} + + +Assert::error( + fn() => checkPresenter(ChildPresenter::class), + E_USER_WARNING, + 'Method OverridingPresenter::createRequest() is no longer called, overriding it has no effect; links are generated by Nette\Application\LinkGenerator.', +); + +Assert::noError(fn() => checkPresenter(Application\UI\Presenter::class)); From 0fea21333662ccb2cdd7ae5699ea1b7e688831c4 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 18:45:57 +0100 Subject: [PATCH 30/54] RoutingPanel: status colors without !important, so they can be overridden --- src/Bridges/ApplicationTracy/dist/panel.phtml | 12 ++++++------ src/Bridges/ApplicationTracy/panel.latte | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Bridges/ApplicationTracy/dist/panel.phtml b/src/Bridges/ApplicationTracy/dist/panel.phtml index 856b1e0a3..a16d5796f 100644 --- a/src/Bridges/ApplicationTracy/dist/panel.phtml +++ b/src/Bridges/ApplicationTracy/dist/panel.phtml @@ -53,16 +53,16 @@ echo ' padding: 2px 5px; } - #tracy-debug .nette-RoutingPanel-status-yes { - background: #BDE678 !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-yes { + background: #BDE678; } - #tracy-debug .nette-RoutingPanel-status-may { - background: #C1D3FF !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-may { + background: #C1D3FF; } - #tracy-debug .nette-RoutingPanel-status-error { - background: #ffd2c3 !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-error { + background: #ffd2c3; } #tracy-debug .nette-RoutingPanel-symbol { diff --git a/src/Bridges/ApplicationTracy/panel.latte b/src/Bridges/ApplicationTracy/panel.latte index 080482c1c..07d4c6c5e 100644 --- a/src/Bridges/ApplicationTracy/panel.latte +++ b/src/Bridges/ApplicationTracy/panel.latte @@ -51,16 +51,16 @@ padding: 2px 5px; } - #tracy-debug .nette-RoutingPanel-status-yes { - background: #BDE678 !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-yes { + background: #BDE678; } - #tracy-debug .nette-RoutingPanel-status-may { - background: #C1D3FF !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-may { + background: #C1D3FF; } - #tracy-debug .nette-RoutingPanel-status-error { - background: #ffd2c3 !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-error { + background: #ffd2c3; } #tracy-debug .nette-RoutingPanel-symbol { From d8891c3a7b0ee242960a7f7b3725e0d0d27337d7 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 18:46:44 +0100 Subject: [PATCH 31/54] Presenter: layout path detection uses strpbrk() instead of a regexp --- src/Application/UI/Presenter.php | 2 +- tests/UI/Presenter.formatLayoutTemplateFiles.phpt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 3ac828e13..044909aee 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -585,7 +585,7 @@ public function findLayoutTemplateFile(): ?string */ public function formatLayoutTemplateFiles(): array { - if (is_string($this->layout) && preg_match('#/|\\\#', $this->layout)) { + if (is_string($this->layout) && strpbrk($this->layout, '/\\') !== false) { return [$this->layout]; } diff --git a/tests/UI/Presenter.formatLayoutTemplateFiles.phpt b/tests/UI/Presenter.formatLayoutTemplateFiles.phpt index 792e51693..8efd269fd 100644 --- a/tests/UI/Presenter.formatLayoutTemplateFiles.phpt +++ b/tests/UI/Presenter.formatLayoutTemplateFiles.phpt @@ -97,4 +97,7 @@ test('explicit layout file assignment', function () { Assert::same([ __DIR__ . '/file.latte', ], $presenter->formatLayoutTemplateFiles()); + + $presenter->setLayout('dir\file.latte'); + Assert::same(['dir\file.latte'], $presenter->formatLayoutTemplateFiles()); }); From 2d0b446148b95ddf9e4429de4ddaf04c20cfabfb Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 18:47:02 +0100 Subject: [PATCH 32/54] tests: LinkGenerator with absolute destination --- tests/Routers/LinkGenerator.phpt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Routers/LinkGenerator.phpt b/tests/Routers/LinkGenerator.phpt index 8b2de4646..2db40179e 100644 --- a/tests/Routers/LinkGenerator.phpt +++ b/tests/Routers/LinkGenerator.phpt @@ -51,6 +51,7 @@ namespace { $generator = new LinkGenerator(new Routers\SimpleRouter, new Http\UrlScript('http://nette.org/en/'), $pf); Assert::same('http://nette.org/en/?action=default&presenter=Homepage', $generator->link('Homepage:default')); Assert::same('http://nette.org/en/?action=default&presenter=Module%3AMy', $generator->link('Module:My:default')); + Assert::same('http://nette.org/en/?action=default&presenter=Module%3AMy', $generator->link(':Module:My:default')); Assert::same('http://nette.org/en/?presenter=Module%3AMy', $generator->link('Module:My:')); Assert::same('http://nette.org/en/?action=default&presenter=Homepage', $generator->link('Homepage:')); Assert::same('http://nette.org/en/?a=10&action=default&presenter=Homepage', $generator->link('Homepage:', [10])); From 9dfbfed9da43cae434de39ff454f0ee937a8023d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 2 Jul 2026 18:37:09 +0200 Subject: [PATCH 33/54] UI\Form: compatible with both nette/forms 3.x and the upcoming 4.0 Two forward-compatible additions to the classic presenter form: - The form owns its cross-origin state (private ?FetchSite $allowedOrigin + allowCrossOrigin()) instead of relying on the protected $crossOrigin inherited from Nette\Forms\Form. Forms 3.x declares that property untyped while 4.0 removes it, so no redeclaration can satisfy both; a differently named private property works with either version. - createDefaultSource() returns null, which under forms 4.0 prevents the lazy default HttpSource from materializing (the form is anchored via the presenter). Under 3.x the method is unused; its return type references an interface that only exists in 4.0, which is safe because without a parent method there is no variance check. The classic receiveHttpData()/isAnchored()/beforeRender() overrides double as the documented BC seam of forms 4.0, so a single class works with both major versions. --- composer.json | 2 +- src/Application/UI/Form.php | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 581e2706a..924633868 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nette/tester": "^2.6", "nette/caching": "^3.2", "nette/di": "^3.2", - "nette/forms": "^3.2", + "nette/forms": "^3.2 || ^4.0", "nette/robot-loader": "^4.0", "nette/security": "^3.2", "latte/latte": "^3.1.4", diff --git a/src/Application/UI/Form.php b/src/Application/UI/Form.php index 6468bb3e3..daa408f3b 100644 --- a/src/Application/UI/Form.php +++ b/src/Application/UI/Form.php @@ -18,6 +18,9 @@ class Form extends Nette\Forms\Form implements SignalReceiver /** @var array Occurs when form is attached to presenter */ public array $onAnchor = []; + /** which Sec-Fetch-Site values are accepted for submission; null disables the check */ + private ?Nette\Http\FetchSite $allowedOrigin = Nette\Http\FetchSite::SameOrigin; + public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null) { @@ -26,6 +29,17 @@ public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?st } + /** + * Forward compatibility with nette/forms 4.0: no default submission source materializes, + * the form is anchored via the presenter and detects submission in receiveHttpData(). + * Under nette/forms 3.x the method is unused. + */ + protected function createDefaultSource(): ?Nette\Forms\SubmissionSource + { + return null; + } + + protected function validateParent(Nette\ComponentModel\IContainer $parent): void { parent::validateParent($parent); @@ -89,6 +103,15 @@ public function isAnchored(): bool } + /** + * Disables the same-origin (Sec-Fetch) CSRF check, allowing cross-origin form submissions. + */ + public function allowCrossOrigin(): void + { + $this->allowedOrigin = null; + } + + #[\Deprecated('use allowCrossOrigin()')] public function disableSameSiteProtection(): void { @@ -141,7 +164,7 @@ public function signalReceived(string $signal): void $class = static::class; throw new BadSignalException("Missing handler for signal '$signal' in $class."); - } elseif (!$this->crossOrigin && !$presenter->getHttpRequest()->isFrom(Nette\Http\FetchSite::SameOrigin)) { + } elseif ($this->allowedOrigin && !$presenter->getHttpRequest()->isFrom($this->allowedOrigin)) { $presenter->detectedCsrf(); } elseif (!$presenter->getRequest()->hasFlag(Nette\Application\Request::Restored)) { From e819ce3e335ac69b1b24e96947c5c6a23d10d7b8 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 2 Jul 2026 15:18:17 +0200 Subject: [PATCH 34/54] UI\Form: adapted to the SubmissionSource model of nette/forms 4.0 The form no longer overrides receiveHttpData(), isAnchored() and beforeRender(); instead it overrides createDefaultSource() to return null and sets the @internal FormSubmissionSource when anchored to a presenter. The source detects submission via the 'submit' signal, reads the application request and installs the hidden signal field in its prepare() render hook; it resolves the presenter from the form lazily, so a form re-anchored to another presenter keeps working. The catch-up pass of setSubmissionSource() replaces the manual loadHttpData() loop in validateParent(). The form owns its cross-origin state (private ?FetchSite $allowedOrigin + allowCrossOrigin()) independently of nette/forms; the same-origin check intentionally stays in signalReceived(), where a failure is reported via Presenter::detectedCsrf(). Requires nette/forms 4.0. --- composer.json | 4 +- phpstan.neon | 5 -- src/Application/UI/Form.php | 56 ++------------------- src/Application/UI/FormSubmissionSource.php | 51 +++++++++++++++++++ 4 files changed, 56 insertions(+), 60 deletions(-) create mode 100644 src/Application/UI/FormSubmissionSource.php diff --git a/composer.json b/composer.json index 924633868..bde7be4e4 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nette/tester": "^2.6", "nette/caching": "^3.2", "nette/di": "^3.2", - "nette/forms": "^3.2 || ^4.0", + "nette/forms": "^4.0", "nette/robot-loader": "^4.0", "nette/security": "^3.2", "latte/latte": "^3.1.4", @@ -39,7 +39,7 @@ "conflict": { "nette/caching": "<3.2", "nette/di": "<3.2", - "nette/forms": "<3.2", + "nette/forms": "<4.0", "nette/schema": "<1.3", "latte/latte": "<3.1.5 || >=3.2", "tracy/tracy": "<2.12" diff --git a/phpstan.neon b/phpstan.neon index 3336ce52a..2db192864 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -84,11 +84,6 @@ parameters: count: 1 path: src/Application/UI/Presenter.php - # Nette Forms compatibility - methods differ between versions - - - identifier: method.notFound - path: src/Application/UI/Form.php - # Control::$template->flashes is set dynamically for flash messages - identifier: property.notFound diff --git a/src/Application/UI/Form.php b/src/Application/UI/Form.php index daa408f3b..caec7b3a9 100644 --- a/src/Application/UI/Form.php +++ b/src/Application/UI/Form.php @@ -17,8 +17,6 @@ class Form extends Nette\Forms\Form implements SignalReceiver { /** @var array Occurs when form is attached to presenter */ public array $onAnchor = []; - - /** which Sec-Fetch-Site values are accepted for submission; null disables the check */ private ?Nette\Http\FetchSite $allowedOrigin = Nette\Http\FetchSite::SameOrigin; @@ -30,9 +28,7 @@ public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?st /** - * Forward compatibility with nette/forms 4.0: no default submission source materializes, - * the form is anchored via the presenter and detects submission in receiveHttpData(). - * Under nette/forms 3.x the method is unused. + * The form is anchored via a presenter; the source is set when the form is attached to it. */ protected function createDefaultSource(): ?Nette\Forms\SubmissionSource { @@ -53,13 +49,8 @@ protected function validateParent(Nette\ComponentModel\IContainer $parent): void $this->setAction(new Link($presenter, 'this')); } - $controls = $this->getControls(); - if (iterator_count($controls) && $this->isSubmitted()) { - foreach ($controls as $control) { - if (!$control->isDisabled()) { - $control->loadHttpData(); - } - } + if (!$this->isAnchored()) { // the write-once source survives re-anchoring, it resolves the current presenter itself + $this->setSubmissionSource(new FormSubmissionSource); // lets already attached controls load their values } Nette\Utils\Arrays::invoke($this->onAnchor, $this); @@ -94,15 +85,6 @@ public function hasPresenter(): bool } - /** - * Tells if the form is anchored. - */ - public function isAnchored(): bool - { - return (bool) $this->getPresenter(throw: false); - } - - /** * Disables the same-origin (Sec-Fetch) CSRF check, allowing cross-origin form submissions. */ @@ -119,38 +101,6 @@ public function disableSameSiteProtection(): void } - /** - * Internal: returns submitted HTTP data or null when form was not submitted. - */ - protected function receiveHttpData(): ?array - { - $presenter = $this->getPresenter(); - if (!$presenter->isSignalReceiver($this, 'submit')) { - return null; - } - - $request = $presenter->getRequest(); - if ($request->isMethod('forward') || $request->isMethod('post') !== $this->isMethod('post')) { - return null; - } - - return $this->isMethod('post') - ? Nette\Utils\Arrays::mergeTree($request->getPost(), $request->getFiles()) - : $request->getParameters(); - } - - - protected function beforeRender(): void - { - parent::beforeRender(); - $key = ($this->isMethod('post') ? '_' : '') . Presenter::SignalKey; - if (!isset($this[$key]) && $this->getAction() !== '') { - $do = $this->lookupPath(Presenter::class) . self::NameSeparator . 'submit'; - $this[$key] = (new Nette\Forms\Controls\HiddenField($do))->setOmitted(); - } - } - - /********************* interface SignalReceiver ****************d*g**/ diff --git a/src/Application/UI/FormSubmissionSource.php b/src/Application/UI/FormSubmissionSource.php new file mode 100644 index 000000000..8a527717d --- /dev/null +++ b/src/Application/UI/FormSubmissionSource.php @@ -0,0 +1,51 @@ +lookup(Presenter::class); + if (!$presenter->isSignalReceiver($form, 'submit')) { + return null; + } + + $request = $presenter->getRequest(); + if ($request->isMethod('forward') || $request->isMethod('post') !== $form->isMethod('post')) { + return null; + } + + return $form->isMethod('post') + ? Nette\Utils\Arrays::mergeTree($request->getPost(), $request->getFiles()) + : $request->getParameters(); + } + + + /** + * Installs the hidden field carrying the 'submit' signal. + */ + public function prepare(Nette\Forms\Form $form): void + { + $key = ($form->isMethod('post') ? '_' : '') . Presenter::SignalKey; + if (!isset($form[$key]) && $form->getAction() !== '') { + $do = $form->lookupPath(Presenter::class) . Nette\ComponentModel\IComponent::NameSeparator . 'submit'; + $form[$key] = (new Nette\Forms\Controls\HiddenField($do))->setOmitted(); + } + } +} From 3037a57435b7c67bb8fa0e1d6888432944adf64a Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 14 Dec 2023 22:21:35 +0100 Subject: [PATCH 35/54] opened 4.0-dev --- composer.json | 2 +- readme.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index bde7be4e4..0354bea3f 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "4.0-dev" } }, "config": { diff --git a/readme.md b/readme.md index 41ddf658c..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.3)](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) From b8ee2f9b7691cf6252e4ea73ab70752c56d7f252 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 21 Jan 2026 07:50:20 +0100 Subject: [PATCH 36/54] composer: increased dependencies versions --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0354bea3f..d065a7193 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ ], "require": { "php": "8.3 - 8.6", - "nette/component-model": "^3.2 || ^4.0", + "nette/component-model": "^4.0", "nette/http": "^3.4", "nette/routing": "^3.1.5", "nette/utils": "^4.1" From 36270c5a13da7499ddabce9150157ad856741cdb Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 8 Feb 2024 21:39:57 +0100 Subject: [PATCH 37/54] added type hints (BC break) --- src/Application/UI/Component.php | 9 +++----- src/Application/UI/Presenter.php | 21 +++++++------------ .../ApplicationLatte/TemplateFactory.php | 2 +- tests/UI/Component.redirect().phpt | 2 +- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index 49c225ab7..1273a6d80 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -357,10 +357,9 @@ 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 * @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 @@ -381,10 +380,9 @@ 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 * @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 @@ -403,9 +401,8 @@ public function redirectPermanent(string $destination, $args = []): void /** * Throws HTTP error. * @throws Nette\Application\BadRequestException - * @return never */ - 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); } diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 044909aee..0d76f4a28 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -512,9 +512,8 @@ 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); @@ -704,9 +703,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())); } @@ -715,9 +713,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)); } @@ -729,9 +726,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(); @@ -741,9 +737,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; } @@ -753,9 +748,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)); @@ -777,9 +771,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; diff --git a/src/Bridges/ApplicationLatte/TemplateFactory.php b/src/Bridges/ApplicationLatte/TemplateFactory.php index 4f51f3fb1..0ed2ac622 100644 --- a/src/Bridges/ApplicationLatte/TemplateFactory.php +++ b/src/Bridges/ApplicationLatte/TemplateFactory.php @@ -41,7 +41,7 @@ public function __construct( * @param ?class-string $class * @return ($class is null ? Template : 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)) { diff --git a/tests/UI/Component.redirect().phpt b/tests/UI/Component.redirect().phpt index 7fe067d35..16e593305 100644 --- a/tests/UI/Component.redirect().phpt +++ b/tests/UI/Component.redirect().phpt @@ -20,7 +20,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); } From c2b4beb2c5e986029d463e8e541edb8ba47f6ec6 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 12 May 2024 20:35:11 +0200 Subject: [PATCH 38/54] Presenter::handleInvalidLink() -> processInvalidLink() (BC break) --- src/Application/UI/Component.php | 2 +- src/Application/UI/Presenter.php | 2 +- src/Bridges/ApplicationDI/ApplicationExtension.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index 1273a6d80..67a22382a 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -311,7 +311,7 @@ public function link(string $destination, $args = []): string return $this->getPresenter()->getLinkGenerator()->link($destination, $args, $this, 'link'); } catch (InvalidLinkException $e) { - return $this->getPresenter()->handleInvalidLink($e); + return $this->getPresenter()->processInvalidLink($e); } } diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 0d76f4a28..c16440bfa 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -898,7 +898,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; diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php index fc55c7dbc..9b25d972c 100644 --- a/src/Bridges/ApplicationDI/ApplicationExtension.php +++ b/src/Bridges/ApplicationDI/ApplicationExtension.php @@ -313,7 +313,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), From 57d3049bf39899bf153324ee27c7c2ba671d6ef8 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 27 Oct 2026 19:52:55 +0100 Subject: [PATCH 39/54] removed legacy services names (BC break) --- src/Bridges/ApplicationDI/ApplicationExtension.php | 5 ----- src/Bridges/ApplicationDI/LatteExtension.php | 5 ----- src/Bridges/ApplicationDI/RoutingExtension.php | 4 ---- 3 files changed, 14 deletions(-) diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php index 9b25d972c..417caf104 100644 --- a/src/Bridges/ApplicationDI/ApplicationExtension.php +++ b/src/Bridges/ApplicationDI/ApplicationExtension.php @@ -133,11 +133,6 @@ public function loadConfiguration(): void ->setCreator(Nette\Application\LinkGenerator::class, [ 1 => new Definitions\Statement('@Nette\Http\IRequest::getUrl'), ]); - - if ($this->name === 'application') { - $builder->addAlias('application', $this->prefix('application')); - $builder->addAlias('nette.presenterFactory', $this->prefix('presenterFactory')); - } } diff --git a/src/Bridges/ApplicationDI/LatteExtension.php b/src/Bridges/ApplicationDI/LatteExtension.php index 1709604ed..4b8c839e6 100644 --- a/src/Bridges/ApplicationDI/LatteExtension.php +++ b/src/Bridges/ApplicationDI/LatteExtension.php @@ -96,11 +96,6 @@ public function loadConfiguration(): void $builder->addDefinition($this->prefix('templateFactory')) ->setCreator(ApplicationLatte\TemplateFactory::class) ->setArguments(['templateClass' => $config->templateClass]); - - if ($this->name === 'latte') { - $builder->addAlias('nette.latteFactory', $this->prefix('latteFactory')); - $builder->addAlias('nette.templateFactory', $this->prefix('templateFactory')); - } } diff --git a/src/Bridges/ApplicationDI/RoutingExtension.php b/src/Bridges/ApplicationDI/RoutingExtension.php index 360f2cfe4..54ac34211 100644 --- a/src/Bridges/ApplicationDI/RoutingExtension.php +++ b/src/Bridges/ApplicationDI/RoutingExtension.php @@ -54,10 +54,6 @@ public function loadConfiguration(): void foreach ($this->config->routes as $mask => $action) { $router->addSetup('$service->addRoute(?, ?)', [$mask, $action]); } - - if ($this->name === 'routing') { - $builder->addAlias('router', $this->prefix('router')); - } } From d583f8984d1301498e7ffdaa877ba290f17105ad Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 24 Oct 2022 19:12:55 +0200 Subject: [PATCH 40/54] Revert "UI\PresenterComponent: removed references created by loadState() for persistent parameters. [Closes nette/nette#703][Closes nette/nette#703][Closes #69]" (possible BC break) This reverts commit cda17f460d020b0f042364d4e140742022a7e94d. See https://forum.nette.org/cs/35528-stejne-pojmenovany-parametr-akce-presenteru-a-persistentni-odlisne-chovani-v-nette-2-0-oproti-aktualnimu#p221742 BC break: Property must be nullable, ie: #[Persistent] public ?int $foo --- docs/internals/persistent-state.md | 6 +++--- src/Application/UI/Component.php | 14 +++++++------- tests/UI/Component.loadState().phpt | 22 +++++++++++++++++++++- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/docs/internals/persistent-state.md b/docs/internals/persistent-state.md index 7b3181815..665b66b32 100644 --- a/docs/internals/persistent-state.md +++ b/docs/internals/persistent-state.md @@ -22,9 +22,9 @@ presenter — a `since` (the declaring class, used to scope which links may carr `ComponentReflection::getParameters()` returns both `#[Persistent]` and `#[Parameter]` properties, so `loadState()` fills and type-validates **both** the -same way (a bad value → `error()` → 404). A missing `#[Parameter]` whose property -is still uninitialized is required: non-nullable → `error()` → 404, nullable → -`null`; a missing `#[Persistent]` leaves its property untouched. The asymmetry is on the way out: only +same way (a bad value → `error()` → 404). A missing parameter whose property is +still uninitialized is required: non-nullable → `error()` → 404, nullable → `null`. +The property and `$params` entry are bound by reference. The asymmetry is on the way out: only persistent entries carry a `since` key, and `getPersistentParams()` filters on its presence — a `#[Parameter]` property is never written back by `saveState` and never reaches a URL. diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index 67a22382a..c67a1f534 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -164,13 +164,13 @@ public function loadState(array $params): void )); } - $this->$name = $params[$name]; + $this->$name = &$params[$name]; - } elseif (isset($this->$name) || array_key_exists('since', $meta)) { - $params[$name] = $this->$name ?? null; - - } elseif ((new \ReflectionProperty($this, $name))->getType()?->allowsNull() ?? true) { - $params[$name] = $this->$name = null; + } elseif ( + isset($this->$name) + || ((new \ReflectionProperty($this, $name))->getType()?->allowsNull() ?? true) + ) { + $params[$name] = &$this->$name; } else { $this->error(sprintf( @@ -254,7 +254,7 @@ final public function getParameter(string $name): mixed */ final public function getParameters(): array { - return $this->params; + return array_map(fn($item) => $item, $this->params); } diff --git a/tests/UI/Component.loadState().phpt b/tests/UI/Component.loadState().phpt index b4c3b41d8..bd7308eca 100644 --- a/tests/UI/Component.loadState().phpt +++ b/tests/UI/Component.loadState().phpt @@ -23,7 +23,14 @@ class TestPresenter extends Application\UI\Presenter public string $default = 'def'; #[Persistent] - public int $persistent; + public ?int $persistent; +} + + +class RequiredPersistentPresenter extends Application\UI\Presenter +{ + #[Persistent] + public int $page; } @@ -54,3 +61,16 @@ test('initialized property', function () { $presenter->loadState([]); Assert::same('xyz', $presenter->required); }); + + +test('missing persistent parameter without default value', function () { + $presenter = new RequiredPersistentPresenter; + Assert::exception( + fn() => $presenter->loadState([]), + Application\BadRequestException::class, + "Missing parameter 'page' required by presenter .", + ); + + $presenter->loadState(['page' => '2']); + Assert::same(2, $presenter->page); +}); From 2cf00895eec37dfad5167de39d686102cde88780 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 20 Apr 2024 02:43:19 +0200 Subject: [PATCH 41/54] Component: method checkRequirements() is called for createComponent() methods (BC break) --- docs/internals/access-control.md | 5 ++--- src/Application/UI/Component.php | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/internals/access-control.md b/docs/internals/access-control.md index c0125bbe6..5a56b8baf 100644 --- a/docs/internals/access-control.md +++ b/docs/internals/access-control.md @@ -15,9 +15,8 @@ No single file shows this; the policy is applied wherever user code is about to 3. **Factory methods** — `Component::createComponent()` checks the policy on the `createComponent()` method, so a component can be guarded at creation. -At the first two sites the (older, unrelated) `checkRequirements()` user hook is -also called with the same reflection — the two mechanisms coexist. The factory -site runs only the attribute policy. +At all three sites the (older, unrelated) `checkRequirements()` user hook is +also called with the same reflection — the two mechanisms coexist. ## `#[Requires]` semantics diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index c67a1f534..f22a65a5a 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -74,7 +74,9 @@ public function getUniqueId(): string protected function createComponent(string $name): ?Nette\ComponentModel\IComponent { if (method_exists($this, $method = 'createComponent' . $name)) { - (new AccessPolicy(new \ReflectionMethod($this, $method)))->checkAccess($this); + $rm = new \ReflectionMethod($this, $method); + (new AccessPolicy($rm))->checkAccess($this); + $this->checkRequirements($rm); } $res = parent::createComponent($name); if ($res && !$res instanceof SignalReceiver && !$res instanceof StatePersistent) { @@ -125,7 +127,7 @@ protected function tryCall(string $method, array $params): bool /** * Descendant can override this method to check for permissions. - * It is called with the presenter class and the render*(), action*(), and handle*() methods. + * It is called with the presenter class and the render*(), action*(), handle*() and createComponent*() methods. * @param \ReflectionClass|\ReflectionMethod $element */ public function checkRequirements(\ReflectionClass|\ReflectionMethod $element): void From e47300eabe2250779950a2a4f36a9a7c2df2cce7 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 7 Apr 2024 04:33:34 +0200 Subject: [PATCH 42/54] Component: only UI components can be added to presenter/component (BC break) WIP --- src/Application/UI/Component.php | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index f22a65a5a..d295a891f 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -71,6 +71,20 @@ 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); + } + + protected function createComponent(string $name): ?Nette\ComponentModel\IComponent { if (method_exists($this, $method = 'createComponent' . $name)) { @@ -78,13 +92,7 @@ protected function createComponent(string $name): ?Nette\ComponentModel\ICompone (new AccessPolicy($rm))->checkAccess($this); $this->checkRequirements($rm); } - $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."); - } - - return $res; + return parent::createComponent($name); } From b1a58bcd470f91c7300efba94805a9c0c16a36a5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 12 Dec 2023 00:11:10 +0100 Subject: [PATCH 43/54] Presenter: removed constructor (BC break!) --- src/Application/UI/Presenter.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index c16440bfa..23b6578ad 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -121,11 +121,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 ?? throw new Nette\InvalidStateException('Request is not set, presenter is not running.'); From 385455caf3f136b2bb28c2e91e603ec6c1246a28 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 30 Nov 2025 00:07:16 +0100 Subject: [PATCH 44/54] removed support for @annotations (BC break) --- src/Application/LinkGenerator.php | 4 +- src/Application/UI/AccessPolicy.php | 1 - src/Application/UI/ComponentReflection.php | 82 +------------------ src/Application/UI/MethodReflection.php | 31 ------- .../ComponentReflection.parseAnnotation.phpt | 41 ---------- 5 files changed, 4 insertions(+), 155 deletions(-) delete mode 100644 src/Application/UI/MethodReflection.php delete mode 100644 tests/UI/ComponentReflection.parseAnnotation.phpt diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index 33cd24244..f2962ade7 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -347,9 +347,7 @@ private function validateLinkTarget( : " from '{$presenter->getName()}:{$presenter->getAction()}'"; if ($mode !== 'forward' && $mode !== 'test' && !(new UI\AccessPolicy($element))->isLinkable()) { throw new UI\InvalidLinkException("Link to forbidden $message."); - } elseif ($presenter?->invalidLinkMode - && (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class)) // dresscode:ignore upgrading.declarations.deprecatedMember -- @deprecated is still supported - ) { + } elseif ($presenter?->invalidLinkMode && $element->getAttributes(Attributes\Deprecated::class)) { trigger_error("Link to deprecated $message.", E_USER_DEPRECATED); } } diff --git a/src/Application/UI/AccessPolicy.php b/src/Application/UI/AccessPolicy.php index 66c8efecb..5c68a0e82 100644 --- a/src/Application/UI/AccessPolicy.php +++ b/src/Application/UI/AccessPolicy.php @@ -72,7 +72,6 @@ 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') // dresscode:ignore upgrading.declarations.deprecatedMember -- @crossOrigin is still supported && !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->sameOrigin === false) ) { $attrs[] = new Attributes\Requires(sameOrigin: true); diff --git a/src/Application/UI/ComponentReflection.php b/src/Application/UI/ComponentReflection.php index 6a6029f3a..47f68230b 100644 --- a/src/Application/UI/ComponentReflection.php +++ b/src/Application/UI/ComponentReflection.php @@ -48,10 +48,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), @@ -92,7 +89,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 @@ -104,9 +101,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 = [...$names, ...$class::getPersistentComponents()]; $components = array_fill_keys($names, ['since' => $class]); @@ -178,77 +173,6 @@ public function getSignalMethod(string $signal): ?\ReflectionMethod } - /** - * Returns an annotation value. - * @deprecated - * @return ?list - */ - 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; - } - } - - $alt = match ($name) { - 'persistent' => '#[Nette\Application\Attributes\Persistent]', - 'deprecated' => '#[Nette\Application\Attributes\Deprecated]', - 'crossOrigin' => '#[Nette\Application\Attributes\Requires(sameOrigin: false)]', - default => 'alternative', - }; - trigger_error("Annotation @$name is deprecated, use $alt (used in " . Reflection::toString($ref) . ')', E_USER_DEPRECATED); - return $res; - } - - - #[\Deprecated] - public function hasAnnotation(string $name): bool - { - return (bool) self::parseAnnotation($this, $name); - } - - - #[\Deprecated] - 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 list - */ - 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 - * @param array $args - * @return list - */ #[\Deprecated] public static function combineArgs(\ReflectionFunctionAbstract $method, array $args): array { diff --git a/src/Application/UI/MethodReflection.php b/src/Application/UI/MethodReflection.php deleted file mode 100644 index 3bd60cdae..000000000 --- a/src/Application/UI/MethodReflection.php +++ /dev/null @@ -1,31 +0,0 @@ - Date: Wed, 22 Apr 2026 17:08:48 +0200 Subject: [PATCH 45/54] RoutingPanel: added support for AI agents --- phpstan.neon | 7 + src/Bridges/ApplicationTracy/RoutingPanel.php | 13 ++ .../ApplicationTracy/dist/panel.agent.phtml | 131 ++++++++++++++++++ .../ApplicationTracy/panel.agent.latte | 62 +++++++++ .../RoutingPanel.getAgentInfo().phpt | 105 ++++++++++++++ 5 files changed, 318 insertions(+) create mode 100644 src/Bridges/ApplicationTracy/dist/panel.agent.phtml create mode 100644 src/Bridges/ApplicationTracy/panel.agent.latte create mode 100644 tests/Bridges.Tracy/RoutingPanel.getAgentInfo().phpt diff --git a/phpstan.neon b/phpstan.neon index 2db192864..d53f70233 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -195,3 +195,10 @@ parameters: - offsetAccess.notFound - closure.unusedUse - identical.alwaysFalse + + # Tracy routing agent panel: compiled Latte template with recursive block array (self-referencing $_blocks) + - + path: src/Bridges/ApplicationTracy/dist/panel.agent.phtml + identifiers: + - offsetAccess.notFound + - closure.unusedUse diff --git a/src/Bridges/ApplicationTracy/RoutingPanel.php b/src/Bridges/ApplicationTracy/RoutingPanel.php index aee22e113..24c97a908 100644 --- a/src/Bridges/ApplicationTracy/RoutingPanel.php +++ b/src/Bridges/ApplicationTracy/RoutingPanel.php @@ -68,6 +68,19 @@ public function getPanel(): string } + public function getAgentInfo(): string + { + return Nette\Utils\Helpers::capture(function () { + $matched = $this->matched; + $routes = $this->routes; + $source = $this->matched ? $this->findSource() : null; + $url = $this->httpRequest->getUrl(); + $method = $this->httpRequest->getMethod(); + require __DIR__ . '/dist/panel.agent.phtml'; + }); + } + + /** @return array{path: string, domain: ?string, module: string, routes: array} */ private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRequest): array { diff --git a/src/Bridges/ApplicationTracy/dist/panel.agent.phtml b/src/Bridges/ApplicationTracy/dist/panel.agent.phtml new file mode 100644 index 000000000..edd9d73b5 --- /dev/null +++ b/src/Bridges/ApplicationTracy/dist/panel.agent.phtml @@ -0,0 +1,131 @@ + $matched */ +/** @var array{path:string,domain:?string,module:string,routes:array} $routes */ +/** @var \ReflectionClass|\ReflectionMethod|string|null $source */ +/** @var Nette\Http\UrlScript $url */ +/** @var string $method */ +use Nette\Application\UI\Presenter; +echo "\n"; +$_blocks['route'] = function ($route,$path) use (&$_blocks) { + $mark = ['yes' => 'MATCHED', 'may' => 'MAY MATCH', 'no' => 'no match', 'oneway' => 'one-way', 'error' => 'ERROR'][$route->matched] ?? $route->matched /* pos 9:2 */; + $mask = $route->mask !== null ? $path . $route->mask : $route->class /* pos 10:2 */; + $defaults = [] /* pos 11:2 */; + foreach ($route->defaults as $k => $v) /* pos 12:2 */ { + $defaults[] = $k . '=' . (is_string($v) ? $v : (is_scalar($v) ? var_export($v, true) : get_debug_type($v))) /* pos 12:40 */; + } + + echo '- ['; + echo Tracy\Helpers::escapeMd($mark) /* pos 13:6 */; + echo '] `'; + echo $mask /* pos 13:16 */; + echo '`'; + if ($defaults) /* pos 13:33 */ { + echo ' ('; + echo Tracy\Helpers::escapeMd(implode(', ', $defaults)) /* pos 13:49 */; + echo ')'; + } + if ($route->error) /* pos 13:82 */ { + echo ' — '; + echo Tracy\Helpers::escapeMd($route->error->getMessage()) /* pos 13:105 */; + } + echo "\n"; +}; +echo "\n"; +$_blocks['routeList'] = function ($list,$path) use (&$_blocks) { + if ($list['domain']) /* pos 17:2 */ { + echo '- domain: `'; + echo Tracy\Helpers::escapeMd($list['domain']) /* pos 17:31 */; + echo '` +'; + } + if ($list['module']) /* pos 19:2 */ { + echo '- module: `'; + echo Tracy\Helpers::escapeMd($list['module']) /* pos 19:31 */; + echo '` +'; + } + $path .= $list['path'] /* pos 21:2 */; + foreach ($list['routes'] as $router) /* pos 22:2 */ { + if (is_array($router)) /* pos 23:3 */ { + $_blocks['routeList']($router,$path); + } else /* pos 25:3 */ { + $_blocks['route']($router,$path); + } + + } + +}; +echo ' +## Routing + +`'; +echo Tracy\Helpers::escapeMd($method) /* pos 33:2 */; +echo ' '; +echo Tracy\Helpers::escapeMd($url->getAbsoluteUrl()) /* pos 33:12 */; +echo '` + +'; +if ($matched === null) /* pos 35:1 */ { + echo '**Matched:** no route +'; +} else /* pos 37:1 */ { + $target = ($matched[Presenter::PresenterKey] ?? '?') . ':' . ($matched[Presenter::ActionKey] ?? Presenter::DefaultAction) /* pos 38:2 */; + echo '**Matched:** '; + echo Tracy\Helpers::escapeMd($target) /* pos 39:15 */; + if (isset($matched[Presenter::SignalKey])) /* pos 39:24 */ { + echo Tracy\Helpers::escapeMd($matched[Presenter::SignalKey]) /* pos 39:66 */; + echo '!'; + } + echo "\n"; + if (is_string($source)) /* pos 40:2 */ { + echo '**Handler:** '; + echo Tracy\Helpers::escapeMd($source) /* pos 41:16 */; + echo ' (class not found) +'; + } elseif ($source instanceof \ReflectionMethod) /* pos 42:2 */ { + echo '**Handler:** '; + echo Tracy\Helpers::escapeMd($source->getDeclaringClass()->getName()) /* pos 43:16 */; + echo '::'; + echo Tracy\Helpers::escapeMd($source->getName()) /* pos 43:59 */; + echo '() in '; + echo Tracy\Helpers::escapeMd($source->getFileName()) /* pos 43:85 */; + echo ':'; + echo Tracy\Helpers::escapeMd($source->getStartLine()) /* pos 43:110 */; + echo "\n"; + } elseif ($source instanceof \ReflectionClass) /* pos 44:2 */ { + echo '**Handler:** '; + echo Tracy\Helpers::escapeMd($source->getName()) /* pos 45:16 */; + echo ' in '; + echo Tracy\Helpers::escapeMd($source->getFileName()) /* pos 45:40 */; + echo ':'; + echo Tracy\Helpers::escapeMd($source->getStartLine()) /* pos 45:65 */; + echo "\n"; + } + + + $skip = [Presenter::PresenterKey => 1, Presenter::ActionKey => 1, Presenter::SignalKey => 1] /* pos 47:2 */; + $extras = array_diff_key($matched, $skip) /* pos 48:2 */; + if ($extras) /* pos 49:2 */ { + echo '**Parameters:** +'; + foreach ($extras as $key => $value) /* pos 51:3 */ { + echo '- '; + echo Tracy\Helpers::escapeMd($key) /* pos 52:6 */; + echo ' = '; + echo Tracy\Helpers::escapeMd(is_string($value) ? $value : (is_scalar($value) ? var_export($value, true) : get_debug_type($value))) /* pos 52:15 */; + echo "\n"; + + } + + } +} +echo ' +### Routes +'; +if (empty($routes['routes'])) /* pos 58:1 */ { + echo '(none defined) +'; +} else /* pos 60:1 */ { + $_blocks['routeList']($routes,''); +} diff --git a/src/Bridges/ApplicationTracy/panel.agent.latte b/src/Bridges/ApplicationTracy/panel.agent.latte new file mode 100644 index 000000000..8efd97b14 --- /dev/null +++ b/src/Bridges/ApplicationTracy/panel.agent.latte @@ -0,0 +1,62 @@ +{varType ?array $matched} +{varType array{path: string, domain: ?string, module: string, routes: array} $routes} +{varType \ReflectionClass|\ReflectionMethod|string|null $source} +{varType Nette\Http\UrlScript $url} +{varType string $method} +{use Nette\Application\UI\Presenter} + +{define route $route, $path} + {do $mark = [yes => 'MATCHED', may => 'MAY MATCH', no => 'no match', oneway => 'one-way', error => 'ERROR'][$route->matched] ?? $route->matched} + {do $mask = $route->mask !== null ? $path . $route->mask : $route->class} + {do $defaults = []} + {foreach $route->defaults as $k => $v}{do $defaults[] = $k . '=' . (is_string($v) ? $v : (is_scalar($v) ? var_export($v, true) : get_debug_type($v)))}{/foreach} + - [{$mark}] `{$mask|noescape}`{if $defaults} ({=implode(', ', $defaults)}){/if}{if $route->error} — {$route->error->getMessage()}{/if} +{/define} + +{define routeList $list, $path} + {if $list[domain]}- domain: `{$list[domain]}` + {/if} + {if $list[module]}- module: `{$list[module]}` + {/if} + {do $path .= $list[path]} + {foreach $list[routes] as $router} + {if is_array($router)} + {include routeList $router, $path} + {else} + {include route $router, $path} + {/if} + {/foreach} +{/define} + +## Routing + +`{$method} {$url->getAbsoluteUrl()}` + +{if $matched === null} + **Matched:** no route +{else} + {do $target = ($matched[Presenter::PresenterKey] ?? '?') . ':' . ($matched[Presenter::ActionKey] ?? Presenter::DefaultAction)} + **Matched:** {$target}{if isset($matched[Presenter::SignalKey])}{$matched[Presenter::SignalKey]}!{/if} + {if is_string($source)} + **Handler:** {$source} (class not found) + {elseif $source instanceof \ReflectionMethod} + **Handler:** {$source->getDeclaringClass()->getName()}::{$source->getName()}() in {$source->getFileName()}:{$source->getStartLine()} + {elseif $source instanceof \ReflectionClass} + **Handler:** {$source->getName()} in {$source->getFileName()}:{$source->getStartLine()} + {/if} + {do $skip = [Presenter::PresenterKey => 1, Presenter::ActionKey => 1, Presenter::SignalKey => 1]} + {do $extras = array_diff_key($matched, $skip)} + {if $extras} + **Parameters:** + {foreach $extras as $key => $value} + - {$key} = {is_string($value) ? $value : (is_scalar($value) ? var_export($value, true) : get_debug_type($value))} + {/foreach} + {/if} +{/if} + +### Routes +{if empty($routes[routes])} + (none defined) +{else} + {include routeList $routes, ''} +{/if} diff --git a/tests/Bridges.Tracy/RoutingPanel.getAgentInfo().phpt b/tests/Bridges.Tracy/RoutingPanel.getAgentInfo().phpt new file mode 100644 index 000000000..a6d75db5a --- /dev/null +++ b/tests/Bridges.Tracy/RoutingPanel.getAgentInfo().phpt @@ -0,0 +1,105 @@ +addRoute('sign-in', 'Sign:in'); +$router->withPath('admin/') + ->addRoute('', 'Admin:Home:default') + ->addRoute('/', ['module' => 'Admin']); +$router->addRoute('/'); + + +test('matched route', function () use ($router) { + $httpRequest = new Http\Request(new UrlScript('http://example.com/admin', '/')); + $panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {})); + $panel->getTab(); + + Assert::match( + <<<'XX' + %A% + ## Routing + + `GET http://example.com/admin` + + **Matched:** Admin:Home:default + %A% + + ### Routes + - [no match] `sign-in` (presenter=Sign, action=in) + - [MATCHED] `admin/` (presenter=Admin:Home, action=default) + - [no match] `admin//` (module=Admin, action=default) + - [MAY MATCH] `/` (action=default) + XX, + $panel->getAgentInfo(), + ); +}); + + +test('no matching route', function () { + $router = new RouteList; + $router->addRoute('sign-in', 'Sign:in'); + + $httpRequest = new Http\Request(new UrlScript('http://example.com/nowhere', '/')); + $panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {})); + $panel->getTab(); + + Assert::match( + <<<'XX' + %A% + ## Routing + + `GET http://example.com/nowhere` + + **Matched:** no route + + ### Routes + - [no match] `sign-in` (presenter=Sign, action=in) + XX, + $panel->getAgentInfo(), + ); +}); + + +test('no routes defined', function () { + $router = new RouteList; + + $httpRequest = new Http\Request(new UrlScript('http://example.com/', '/')); + $panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {})); + $panel->getTab(); + + Assert::match( + <<<'XX' + %A% + ## Routing + + `GET http://example.com/` + + **Matched:** no route + + ### Routes + (none defined) + XX, + $panel->getAgentInfo(), + ); +}); + + +test('matched with extra parameters', function () use ($router) { + $httpRequest = new Http\Request(new UrlScript('http://example.com/article/detail?id=42', '/')); + $panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {})); + $panel->getTab(); + + $output = $panel->getAgentInfo(); + Assert::contains('**Matched:** Article:detail', $output); + Assert::contains('**Parameters:**', $output); + Assert::contains('- id = 42', $output); +}); From 04f1ab29c1fd6c47525a7970837229b014e9443d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 22 Jan 2024 00:20:18 +0100 Subject: [PATCH 46/54] uses nette/routing 4.0 --- src/Application/Routers/RouteList.php | 4 ++-- src/Bridges/ApplicationTracy/RoutingPanel.php | 2 +- tests/Routers/RouteList.addRoute.phpt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Application/Routers/RouteList.php b/src/Application/Routers/RouteList.php index 3349bca3c..8ae7cc88c 100644 --- a/src/Application/Routers/RouteList.php +++ b/src/Application/Routers/RouteList.php @@ -70,10 +70,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; } diff --git a/src/Bridges/ApplicationTracy/RoutingPanel.php b/src/Bridges/ApplicationTracy/RoutingPanel.php index 24c97a908..ba6b77342 100644 --- a/src/Bridges/ApplicationTracy/RoutingPanel.php +++ b/src/Bridges/ApplicationTracy/RoutingPanel.php @@ -101,7 +101,7 @@ private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRe continue; } - $matched = $flags[$i] ? 'oneway' : 'no'; + $matched = empty($flags[$i]['oneWay']) ? 'no' : 'oneway'; $params = $e = null; try { if ( diff --git a/tests/Routers/RouteList.addRoute.phpt b/tests/Routers/RouteList.addRoute.phpt index e41357e3f..dc0388bf7 100644 --- a/tests/Routers/RouteList.addRoute.phpt +++ b/tests/Routers/RouteList.addRoute.phpt @@ -9,7 +9,7 @@ require __DIR__ . '/Route.php'; $list = new RouteList; $list->addRoute('foo', ['presenter' => 'foo'], oneWay: 1); -$list->addRoute('bar', ['presenter' => 'bar'], oneWay: 1); +$list->addRoute('bar', ['presenter' => 'bar'], oneWay: true); $list->addRoute('hello', ['presenter' => 'hello']); From 71cc17a03abb245f5b14e74d3923db12b578f8e6 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 19 Apr 2024 19:01:35 +0200 Subject: [PATCH 47/54] removed {templatePrint} --- .../Nodes/TemplatePrintNode.php | 59 ------------------- src/Bridges/ApplicationLatte/UIExtension.php | 1 - 2 files changed, 60 deletions(-) delete mode 100644 src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php diff --git a/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php b/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php deleted file mode 100644 index 4d585e4ea..000000000 --- a/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php +++ /dev/null @@ -1,59 +0,0 @@ -getParameters(), ' . PhpHelpers::dump($this->template ?? Template::class) . '); exit;'; - } - - - /** @param array $params */ - public static function printClass(array $params, string $parentClass): void - { - $bp = new Latte\Essential\Blueprint; - $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) { - $controlFile = (new \ReflectionClass($control))->getFileName() - ?: throw new \LogicException('Cannot determine file of class ' . $control::class . '.'); - $file = dirname($controlFile) . '/' . $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/UIExtension.php b/src/Bridges/ApplicationLatte/UIExtension.php index ed2310cfc..25b338670 100644 --- a/src/Bridges/ApplicationLatte/UIExtension.php +++ b/src/Bridges/ApplicationLatte/UIExtension.php @@ -79,7 +79,6 @@ public function getTags(): array 'plink' => Nodes\LinkNode::create(...), 'link' => Nodes\LinkNode::create(...), 'linkBase' => Nodes\LinkBaseNode::create(...), - 'templatePrint' => Nodes\TemplatePrintNode::create(...), 'snippet' => Nodes\SnippetNode::create(...), 'snippetArea' => Nodes\SnippetAreaNode::create(...), 'layout' => $this->createExtendsNode(...), From f53d5217a76f65483c65332dce058f356ec43bbf Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 13 May 2024 03:16:47 +0200 Subject: [PATCH 48/54] Presenter::argsToParams() now distinguish between NULL and '' --- src/Application/UI/Component.php | 2 +- src/Application/UI/ParameterConverter.php | 6 ++---- tests/UI/Presenter.link().phpt | 9 ++++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index d295a891f..4cbb08eb7 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -238,7 +238,7 @@ public function saveStatePartial(array &$params, ComponentReflection $reflection )); } - if ($params[$name] === $meta['def'] || ($meta['def'] === null && $params[$name] === '')) { + if ($params[$name] === $meta['def']) { $params[$name] = null; // value transmit is unnecessary } } diff --git a/src/Application/UI/ParameterConverter.php b/src/Application/UI/ParameterConverter.php index e83d56d4b..a6a0cf857 100644 --- a/src/Application/UI/ParameterConverter.php +++ b/src/Application/UI/ParameterConverter.php @@ -124,10 +124,8 @@ public static function toParameters( : new InvalidLinkException($message); } - $def = $param->isDefaultValueAvailable() - ? $param->getDefaultValue() - : null; - if ($args[$name] === $def || ($def === null && $args[$name] === '')) { + $def = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; + if ($args[$name] === $def) { $args[$name] = null; // value transmit is unnecessary } } diff --git a/tests/UI/Presenter.link().phpt b/tests/UI/Presenter.link().phpt index ca417fdb3..60afeb7e4 100644 --- a/tests/UI/Presenter.link().phpt +++ b/tests/UI/Presenter.link().phpt @@ -152,7 +152,7 @@ class TestPresenter extends Application\UI\Presenter $this->absoluteUrls = false; // persistent params - Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['pint' => $this->pint, 'p' => ''])); + Assert::same('/index.php?p=&action=params&presenter=Test', $this->link('params', ['pint' => $this->pint, 'p' => ''])); Assert::same( '/index.php?pint=20&parr%5B0%5D=1&action=params&presenter=Test', $this->link('params', ['pint' => $this->pint * 2, 'pbool' => true, 'parr' => [1]]), @@ -245,7 +245,10 @@ class TestPresenter extends Application\UI\Presenter '/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( + '/index.php?mycontrol-y=&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), @@ -317,7 +320,7 @@ class TestPresenter extends Application\UI\Presenter $this->link('params', ['int' => 1, 'bool' => true, 'str' => 'abc', 'arr' => '1']), ); Assert::same( - '/index.php?int=0&bool=0&action=params&presenter=Test', + '/index.php?int=0&bool=0&str=&arr=&action=params&presenter=Test', $this->link('params', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => '']), ); Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['int' => new stdClass])); From 2f45d4ba5d6af1d3e840a662d75d713f4f0ed77d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 21 Oct 2026 22:56:41 +0200 Subject: [PATCH 49/54] AccessPolicy: class-level #[Requires] is inherited from ancestors ReflectionClass::getAttributes() does not return attributes of parent classes, so a rule placed on a common ancestor (a typical #[Requires(sameOrigin: true)] on BaseAdminPresenter) was silently not enforced in its descendants. Attributes are now collected along the whole ancestor chain; a rule explicitly set in a descendant overrides the same rule of its ancestors. --- docs/internals/access-control.md | 6 ++ src/Application/UI/AccessPolicy.php | 46 ++++++++++++++-- tests/UI/Requires.inheritance.phpt | 85 +++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 tests/UI/Requires.inheritance.phpt diff --git a/docs/internals/access-control.md b/docs/internals/access-control.md index 5a56b8baf..7d125d7cf 100644 --- a/docs/internals/access-control.md +++ b/docs/internals/access-control.md @@ -4,6 +4,12 @@ repeatable, class- or method-level, and matched with `IS_INSTANCEOF`, so subclasses (like the deprecated `#[CrossOrigin]`) count as `Requires` instances. +Class-level attributes are **inherited**, unlike PHP attributes in general: +`AccessPolicy` collects them along the whole ancestor chain, so a rule on a base +presenter applies to every descendant. A rule (a property of `Requires`) set +explicitly in a descendant overrides the same rule of all its ancestors; the +other rules keep combining. Method-level attributes follow plain PHP semantics. + ## Enforcement runs at three sites No single file shows this; the policy is applied wherever user code is about to run: diff --git a/src/Application/UI/AccessPolicy.php b/src/Application/UI/AccessPolicy.php index 5c68a0e82..18e136862 100644 --- a/src/Application/UI/AccessPolicy.php +++ b/src/Application/UI/AccessPolicy.php @@ -18,6 +18,8 @@ */ final class AccessPolicy { + /** @var array */ + private static array $classAttrsCache = []; private Presenter $presenter; @@ -56,10 +58,46 @@ public function isLinkable(): bool */ private function getAttributes(): array { - return array_map( - fn($ra) => $ra->newInstance(), - $this->element->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF), - ); + return $this->element instanceof \ReflectionMethod + ? array_map( + fn($ra) => $ra->newInstance(), + $this->element->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF), + ) + : self::$classAttrsCache[$this->element->getName()] ??= $this->getClassAttributes($this->element); + } + + + /** + * Collects class-level attributes including those inherited from ancestor classes. + * A rule explicitly set in a descendant overrides the same rule of all its ancestors. + * @param \ReflectionClass $class + * @return Attributes\Requires[] + */ + private function getClassAttributes(\ReflectionClass $class): array + { + $res = $overridden = []; + for ($rc = $class; $rc; $rc = $rc->getParentClass()) { + $defined = []; + foreach ($rc->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF) as $ra) { + $attr = $ra->newInstance(); + $used = false; + foreach (array_keys(array_filter(get_object_vars($attr), fn($value) => $value !== null)) as $prop) { + if (isset($overridden[$prop])) { + $attr->$prop = null; + } else { + $defined[$prop] = $used = true; + } + } + + if ($used) { + $res[] = $attr; + } + } + + $overridden += $defined; + } + + return $res; } diff --git a/tests/UI/Requires.inheritance.phpt b/tests/UI/Requires.inheritance.phpt new file mode 100644 index 000000000..d4483e814 --- /dev/null +++ b/tests/UI/Requires.inheritance.phpt @@ -0,0 +1,85 @@ +terminate(); + } +} + +#[Requires(forward: false)] +class OverridingRequiresPresenter extends BaseRequiresPresenter +{ + public function actionDefault(): never + { + $this->terminate(); + } +} + +#[Requires(methods: ['POST'])] +class CombiningRequiresPresenter extends BaseRequiresPresenter +{ + public function actionDefault(): never + { + $this->terminate(); + } +} + + +// rule inherited from ancestor is enforced +$presenter = createPresenter(InheritingRequiresPresenter::class); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), +); + +Assert::exception( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), + Application\BadRequestException::class, + 'Forwarded request is required by InheritingRequiresPresenter', +); + + +// rule explicitly set in descendant overrides the ancestor +$presenter = createPresenter(OverridingRequiresPresenter::class); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), +); + + +// descendant rule combines with a different inherited rule +$presenter = createPresenter(CombiningRequiresPresenter::class, method: 'POST'); +Assert::exception( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), + Application\BadRequestException::class, + 'Forwarded request is required by CombiningRequiresPresenter', +); + +$presenter = createPresenter(CombiningRequiresPresenter::class, method: 'POST'); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), +); + +$presenter = createPresenter(CombiningRequiresPresenter::class); +Assert::exception( + fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + Application\BadRequestException::class, + 'Method GET is not allowed by CombiningRequiresPresenter', +); From beeefada46ec662c6bd68e7ac80d1451c2cadc1e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 26 Oct 2026 23:38:52 +0100 Subject: [PATCH 50/54] Control: flashMessage() does not create the template In the most typical flow - flashMessage() followed by redirect() - the template was created just to be thrown away, which builds a whole Latte\Engine instance. The messages are written to the session first and into the template only if it already exists; a template created later gets them from the template factory, which reads the flashes from the session on its own. --- docs/internals/templates.md | 6 ++++ src/Application/UI/Control.php | 10 +++++-- tests/UI/Presenter.flashMessage.phpt | 41 +++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/internals/templates.md b/docs/internals/templates.md index 0f2bfd2a5..31e247838 100644 --- a/docs/internals/templates.md +++ b/docs/internals/templates.md @@ -42,6 +42,12 @@ the default. `TemplateFactory::createTemplate` instantiates it around a fresh La engine and injects the default variables (`user`, `baseUrl`, `basePath`, `flashes`, `control`, `presenter`). +**`flashes` depends on this injection.** `Control::flashMessage()` writes the +message to the flash session and into the template only if the template already +exists; it never creates one. A template created later gets the messages solely +from the factory, which reads them from the flash session, so a custom +`TemplateFactory` has to inject `flashes` the same way. + ## `#[TemplateVariable]` At `sendTemplate`, `completeTemplate()` copies every `#[TemplateVariable]` property diff --git a/src/Application/UI/Control.php b/src/Application/UI/Control.php index 36fa26be7..0a0f9d142 100644 --- a/src/Application/UI/Control.php +++ b/src/Application/UI/Control.php @@ -104,10 +104,14 @@ public function flashMessage(string|\stdClass|\Stringable $message, string $type 'message' => $message, 'type' => $type, ]; - $messages = $this->getPresenter()->getFlashSession()->get($id); + $session = $this->getPresenter()->getFlashSession(); + $messages = $session->get($id); $messages[] = $flash; - $this->getTemplate()->flashes = $messages; - $this->getPresenter()->getFlashSession()->set($id, $messages); + $session->set($id, $messages); + if (isset($this->template)) { // a not-yet-created template gets flashes injected by the template factory + $this->template->flashes = $messages; + } + return $flash; } diff --git a/tests/UI/Presenter.flashMessage.phpt b/tests/UI/Presenter.flashMessage.phpt index 3cb5bac87..30c26d024 100644 --- a/tests/UI/Presenter.flashMessage.phpt +++ b/tests/UI/Presenter.flashMessage.phpt @@ -142,7 +142,7 @@ test('multiple flash messages are stored', function () { }); -test('flash message is available in template', function () { +test('flash message is written to an already created template', function () { $messages = []; $sessionSection = Mockery::mock(Http\SessionSection::class); $sessionSection->shouldReceive('get')->andReturnUsing(function () use (&$messages) { @@ -172,6 +172,45 @@ test('flash message is available in template', function () { $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + $template = $presenter->getTemplate(); + $presenter->flashMessage('Template message'); + + Assert::count(1, $template->flashes); + Assert::same('Template message', $template->flashes[0]->message); +}); + + +test('flash message is injected into a template created later', 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(); + $latteFactory = Mockery::mock(Nette\Bridges\ApplicationLatte\LatteFactory::class); + $latteFactory->shouldReceive('create')->andReturn($latte); + $templateFactory = new Nette\Bridges\ApplicationLatte\TemplateFactory($latteFactory); + + $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(); From faf1f7036da9c2e6b78bc4f8171210c58e3dba3c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 18:50:54 +0100 Subject: [PATCH 51/54] Component: StatePersistent component that is not UI\Component cannot be added (BC break) Its state was silently skipped by Presenter::getGlobalState(). --- src/Application/UI/Component.php | 2 + tests/UI/Component.addComponent().phpt | 52 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/UI/Component.addComponent().phpt diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index 4cbb08eb7..fb247049a 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -79,6 +79,8 @@ public function addComponent( { 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.'); + } elseif ($component instanceof StatePersistent && !$component instanceof self) { + throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' cannot carry persistent state, only ' . self::class . ' can.'); } return parent::addComponent($component, $name, $insertBefore); diff --git a/tests/UI/Component.addComponent().phpt b/tests/UI/Component.addComponent().phpt new file mode 100644 index 000000000..b50e432ce --- /dev/null +++ b/tests/UI/Component.addComponent().phpt @@ -0,0 +1,52 @@ + $control->addComponent(new TestControl, 'ok')); +$control->addComponent(new TestControl, 'first', insertBefore: 'ok'); +Assert::same(['first', 'ok'], array_keys($control->getComponents())); + +Assert::exception( + fn() => $control->addComponent(new ForeignComponent, 'foreign'), + Nette\InvalidStateException::class, + "Component 'foreign' of type ForeignComponent is not intended to be used in the Presenter.", +); + +Assert::exception( + fn() => $control->addComponent(new ForeignStatePersistent, 'state'), + Nette\InvalidStateException::class, + "Component 'state' of type ForeignStatePersistent cannot carry persistent state, only Nette\\Application\\UI\\Component can.", +); From 7012206706dd79dea09ea31f359870b156703444 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 28 Oct 2026 18:52:13 +0100 Subject: [PATCH 52/54] PresenterFactory: mapping accepts submodules like 'Admin:Forum', the longest prefix wins Previously such a key was silently ignored. --- docs/internals/routing.md | 3 ++- src/Application/PresenterFactory.php | 11 ++++++++--- ...PresenterFactory.formatPresenterClass.phpt | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/internals/routing.md b/docs/internals/routing.md index a5ed3868d..82b3f0456 100644 --- a/docs/internals/routing.md +++ b/docs/internals/routing.md @@ -55,7 +55,8 @@ errorPresenter` and anything else to `errorPresenter` (5xx); `sendHttpCode` maps `formatPresenterClass` maps a presenter name to a class through the mapping masks (the hard default is `App\Presentation\*\**Presenter`; `setMapping` overrides), by -substituting `**`→`Part\Part` and `*`→`Part` per `:`-separated segment. Names are +substituting `**`→`Part\Part` and `*`→`Part` per `:`-separated segment. A mapping +key may name a submodule (`Admin:Forum`); the longest matching module prefix wins. Names are charset-validated, the class is checked for `IPresenter`/non-abstract, and resolved classes are cached. Aliases are resolved in the **link** layer (the `@alias` destination), not in routing. diff --git a/src/Application/PresenterFactory.php b/src/Application/PresenterFactory.php index 1686211fb..90bb92024 100644 --- a/src/Application/PresenterFactory.php +++ b/src/Application/PresenterFactory.php @@ -115,9 +115,14 @@ public function formatPresenterClass(string $presenter): string throw new InvalidPresenterException("Presenter name must be alphanumeric string, '$presenter' is invalid."); } $parts = explode(':', $presenter); - $mapping = isset($parts[1], $this->mapping[$parts[0]]) - ? $this->mapping[array_shift($parts)] - : $this->mapping['*']; + $mapping = $this->mapping['*']; + for ($i = count($parts) - 1; $i > 0; $i--) { // the longest module prefix wins + if (isset($this->mapping[$module = implode(':', array_slice($parts, 0, $i))])) { + $mapping = $this->mapping[$module]; + $parts = array_slice($parts, $i); + break; + } + } while ($part = array_shift($parts)) { $mapping[0] .= strtr($mapping[$parts ? 1 : 2], ['**' => "$part\\$part", '*' => $part]); diff --git a/tests/Application/PresenterFactory.formatPresenterClass.phpt b/tests/Application/PresenterFactory.formatPresenterClass.phpt index 67db5c807..32f18aa91 100644 --- a/tests/Application/PresenterFactory.formatPresenterClass.phpt +++ b/tests/Application/PresenterFactory.formatPresenterClass.phpt @@ -91,6 +91,25 @@ test('global wildcard mapping strategy', function () { }); +test('mapping of a submodule', function () { + $factory = new PresenterFactory; + + $factory->setMapping([ + 'Foo' => 'My\App\*Mod\*Presenter', + 'Foo:Bar' => 'Other\App\*Mod\*Presenter', + 'Foo:Bar:Baz' => 'Third\*Presenter', + ]); + + Assert::same('App\Presentation\Foo\FooPresenter', $factory->formatPresenterClass('Foo')); + Assert::same('My\App\BarPresenter', $factory->formatPresenterClass('Foo:Bar')); + Assert::same('My\App\QuxMod\BarPresenter', $factory->formatPresenterClass('Foo:Qux:Bar')); + Assert::same('Other\App\BazPresenter', $factory->formatPresenterClass('Foo:Bar:Baz')); + Assert::same('Other\App\QuxMod\BazPresenter', $factory->formatPresenterClass('Foo:Bar:Qux:Baz')); + Assert::same('Third\QuxPresenter', $factory->formatPresenterClass('Foo:Bar:Baz:Qux')); + Assert::same('App\Presentation\Bar\Baz\BazPresenter', $factory->formatPresenterClass('Bar:Baz')); +}); + + test('array-based hierarchical mapping', function () { $factory = new PresenterFactory; $factory->setMapping([ From b7068cb5fa313ad4c285a716aab2f8996fa93e01 Mon Sep 17 00:00:00 2001 From: Jakub Duchek Date: Thu, 5 Mar 2026 16:22:52 +0100 Subject: [PATCH 53/54] fix: Template::render() accepts object for template parameters (#351) Latte\Engine::render() supports passing an object as template parameters, but Nette\Bridges\ApplicationLatte\Template::render() only accepted arrays. This adds object|array union type to render() and renderToString(). --- src/Bridges/ApplicationLatte/Template.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Bridges/ApplicationLatte/Template.php b/src/Bridges/ApplicationLatte/Template.php index 32378c9e0..adf1ff85a 100644 --- a/src/Bridges/ApplicationLatte/Template.php +++ b/src/Bridges/ApplicationLatte/Template.php @@ -9,6 +9,7 @@ use Latte; use Nette; +use function is_array; /** @@ -33,11 +34,11 @@ final public function getLatte(): Latte\Engine /** * Renders template to output. - * @param array $params + * @param object|array $params */ - public function render(?string $file = null, array $params = []): void + public function render(?string $file = null, object|array $params = []): void { - Nette\Utils\Arrays::toObject($params, $this); + Nette\Utils\Arrays::toObject(is_array($params) ? $params : get_object_vars($params), $this); $file ??= $this->file ?? throw new Nette\InvalidStateException('Template file name was not set.'); $this->latte->render($file, $this); } @@ -45,11 +46,11 @@ public function render(?string $file = null, array $params = []): void /** * Renders template to string. - * @param array $params + * @param object|array $params */ - public function renderToString(?string $file = null, array $params = []): string + public function renderToString(?string $file = null, object|array $params = []): string { - Nette\Utils\Arrays::toObject($params, $this); + Nette\Utils\Arrays::toObject(is_array($params) ? $params : get_object_vars($params), $this); $file ??= $this->file ?? throw new Nette\InvalidStateException('Template file name was not set.'); return $this->latte->renderToString($file, $this); } From 93be5c58a582ef522649734b69961279f2d48610 Mon Sep 17 00:00:00 2001 From: Jakub Duchek Date: Fri, 11 Sep 2026 11:47:42 +0200 Subject: [PATCH 54/54] fixed object template parameter tests --- tests/Bridges.Latte/Template.render.phpt | 32 +++++++++++++++++++ .../TemplateFactory.customTemplate.phpt | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/Bridges.Latte/Template.render.phpt diff --git a/tests/Bridges.Latte/Template.render.phpt b/tests/Bridges.Latte/Template.render.phpt new file mode 100644 index 000000000..b4ecb5ba7 --- /dev/null +++ b/tests/Bridges.Latte/Template.render.phpt @@ -0,0 +1,32 @@ +setLoader(new Latte\Loaders\StringLoader); + return new DefaultTemplate($latte); +} + + +test('render accepts object parameters', function () { + $template = createTemplate(); + ob_start(); + $template->render('{$message}', (object) ['message' => 'Hello']); + Assert::same('Hello', ob_get_clean()); +}); + + +test('renderToString accepts object parameters', function () { + $template = createTemplate(); + Assert::same('Hello', $template->renderToString('{$message}', (object) ['message' => 'Hello'])); +}); diff --git a/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt b/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt index c750e02d7..07647f73a 100644 --- a/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt +++ b/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt @@ -18,7 +18,7 @@ class TemplateMock extends Template private string $file = 'ko'; - public function render(?string $file = null, array $params = []): void + public function render(?string $file = null, object|array $params = []): void { echo strrev($this->file); }