├── src
├── Application
│ ├── UI
│ │ ├── InvalidLinkException.php
│ │ ├── BadSignalException.php
│ │ ├── TemplateFactory.php
│ │ ├── SignalReceiver.php
│ │ ├── Renderable.php
│ │ ├── StatePersistent.php
│ │ ├── Template.php
│ │ ├── Multiplier.php
│ │ ├── Link.php
│ │ ├── AccessPolicy.php
│ │ ├── Form.php
│ │ ├── Control.php
│ │ ├── ParameterConverter.php
│ │ ├── ComponentReflection.php
│ │ └── Component.php
│ ├── IPresenter.php
│ ├── Attributes
│ │ ├── Deprecated.php
│ │ ├── TemplateVariable.php
│ │ ├── Parameter.php
│ │ ├── Persistent.php
│ │ ├── CrossOrigin.php
│ │ └── Requires.php
│ ├── Responses
│ │ ├── VoidResponse.php
│ │ ├── ForwardResponse.php
│ │ ├── CallbackResponse.php
│ │ ├── TextResponse.php
│ │ ├── RedirectResponse.php
│ │ ├── JsonResponse.php
│ │ └── FileResponse.php
│ ├── Response.php
│ ├── IPresenterFactory.php
│ ├── Routers
│ │ ├── SimpleRouter.php
│ │ ├── CliRouter.php
│ │ ├── RouteList.php
│ │ └── Route.php
│ ├── ErrorPresenter.php
│ ├── Helpers.php
│ ├── exceptions.php
│ ├── templates
│ │ └── error.phtml
│ ├── Request.php
│ ├── PresenterFactory.php
│ ├── MicroPresenter.php
│ ├── Application.php
│ └── LinkGenerator.php
├── Bridges
│ ├── ApplicationLatte
│ │ ├── LatteFactory.php
│ │ ├── Nodes
│ │ │ ├── NNonceNode.php
│ │ │ ├── IfCurrentNode.php
│ │ │ ├── LinkBaseNode.php
│ │ │ ├── SnippetAreaNode.php
│ │ │ ├── LinkNode.php
│ │ │ ├── ControlNode.php
│ │ │ └── SnippetNode.php
│ │ ├── DefaultTemplate.php
│ │ ├── TemplateFactory.php
│ │ ├── Template.php
│ │ ├── SnippetRuntime.php
│ │ ├── UIExtension.php
│ │ └── TemplateGenerator.php
│ ├── ApplicationTracy
│ │ ├── tab.latte
│ │ ├── dist
│ │ │ ├── tab.phtml
│ │ │ └── panel.phtml
│ │ ├── RoutingPanel.php
│ │ └── panel.latte
│ └── ApplicationDI
│ │ ├── PresenterFactoryCallback.php
│ │ ├── RoutingExtension.php
│ │ ├── LatteExtension.php
│ │ └── ApplicationExtension.php
└── compatibility-intf.php
├── readme.md
├── composer.json
├── license.md
└── .phpstorm.meta.php
/src/Application/UI/InvalidLinkException.php:
--------------------------------------------------------------------------------
1 | factory = $factory;
27 | }
28 |
29 |
30 | protected function createComponent(string $name): ?Nette\ComponentModel\IComponent
31 | {
32 | return ($this->factory)($name, $this);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/Nodes/NNonceNode.php:
--------------------------------------------------------------------------------
1 | global->uiNonce ? " nonce=\"{$this->global->uiNonce}\"" : "";';
30 | }
31 |
32 |
33 | public function &getIterator(): \Generator
34 | {
35 | false && yield;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Application/Responses/ForwardResponse.php:
--------------------------------------------------------------------------------
1 | request = $request;
26 | }
27 |
28 |
29 | public function getRequest(): Nette\Application\Request
30 | {
31 | return $this->request;
32 | }
33 |
34 |
35 | /**
36 | * Sends response to output.
37 | */
38 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void
39 | {
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Application/Responses/CallbackResponse.php:
--------------------------------------------------------------------------------
1 | callback = $callback;
30 | }
31 |
32 |
33 | /**
34 | * Sends response to output.
35 | */
36 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void
37 | {
38 | ($this->callback)($httpRequest, $httpResponse);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Application/Responses/TextResponse.php:
--------------------------------------------------------------------------------
1 | source = $source;
26 | }
27 |
28 |
29 | public function getSource(): mixed
30 | {
31 | return $this->source;
32 | }
33 |
34 |
35 | /**
36 | * Sends response to output.
37 | */
38 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void
39 | {
40 | if ($this->source instanceof Nette\Application\UI\Template) {
41 | $this->source->render();
42 |
43 | } else {
44 | echo $this->source;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Application/Attributes/Requires.php:
--------------------------------------------------------------------------------
1 | methods = $methods === null ? null : (array) $methods;
30 | $this->actions = $actions === null ? null : (array) $actions;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Application/Responses/RedirectResponse.php:
--------------------------------------------------------------------------------
1 | url = $url;
28 | $this->httpCode = $httpCode;
29 | }
30 |
31 |
32 | public function getUrl(): string
33 | {
34 | return $this->url;
35 | }
36 |
37 |
38 | public function getCode(): int
39 | {
40 | return $this->httpCode;
41 | }
42 |
43 |
44 | /**
45 | * Sends response to output.
46 | */
47 | public function send(Http\IRequest $httpRequest, Http\IResponse $httpResponse): void
48 | {
49 | $httpResponse->redirect($this->url, $this->httpCode);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationTracy/tab.latte:
--------------------------------------------------------------------------------
1 | {use Nette\Application\UI\Presenter}
2 |
3 |
4 |
9 | {if $matched === null}
10 | no route
11 | {elseif isset($matched[Presenter::PresenterKey])}
12 | {$matched[Presenter::PresenterKey]}:{$matched[Presenter::ActionKey] ?? Presenter::DefaultAction}{if isset($matched[Presenter::SignalKey])}
13 | {$matched[Presenter::SignalKey]}!{/if}
14 | {/if}
15 |
16 |
17 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Nette Application MVC
2 | =====================
3 |
4 | [](https://packagist.org/packages/nette/application)
5 | [](https://github.com/nette/application/actions)
6 | [](https://github.com/nette/application/releases)
7 | [](https://github.com/nette/application/blob/master/license.md)
8 |
9 | Model-View-Controller is a software architecture that was created to satisfy the need to separate utility code (controller) from application logic code (model) and from code for displaying data (view) in applications with graphical user interface. With this approach we make the application better understandable, simplify future development and enable testing each unit of the application separately.
10 |
11 | Please, [see documentation](https://doc.nette.org/application).
12 |
13 | If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you!
14 |
--------------------------------------------------------------------------------
/src/Application/Routers/SimpleRouter.php:
--------------------------------------------------------------------------------
1 | $presenter,
35 | 'action' => $action === '' ? Application\UI\Presenter::DefaultAction : $action,
36 | ];
37 | }
38 |
39 | parent::__construct($defaults);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Application/ErrorPresenter.php:
--------------------------------------------------------------------------------
1 | getParameter('exception');
31 | if ($e instanceof Application\BadRequestException) {
32 | $code = $e->getHttpCode();
33 | } else {
34 | $code = 500;
35 | $this->logger?->log($e, ILogger::EXCEPTION);
36 | }
37 |
38 | return new Application\Responses\CallbackResponse(function (Http\IRequest $httpRequest, Http\IResponse $httpResponse) use ($code): void {
39 | if (preg_match('#^text/html(?:;|$)#', (string) $httpResponse->getHeader('Content-Type'))) {
40 | require __DIR__ . '/templates/error.phtml';
41 | }
42 | });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/DefaultTemplate.php:
--------------------------------------------------------------------------------
1 | $name = $value;
41 | return $this;
42 | }
43 |
44 |
45 | /**
46 | * Sets all parameters.
47 | */
48 | public function setParameters(array $params): static
49 | {
50 | return Nette\Utils\Arrays::toObject($params, $this);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Application/Responses/JsonResponse.php:
--------------------------------------------------------------------------------
1 | payload = $payload;
27 | $this->contentType = $contentType ?? 'application/json';
28 | }
29 |
30 |
31 | public function getPayload(): mixed
32 | {
33 | return $this->payload;
34 | }
35 |
36 |
37 | /**
38 | * Returns the MIME content type of a downloaded file.
39 | */
40 | public function getContentType(): string
41 | {
42 | return $this->contentType;
43 | }
44 |
45 |
46 | /**
47 | * Sends response to output.
48 | */
49 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void
50 | {
51 | $httpResponse->setContentType($this->contentType, 'utf-8');
52 | echo Nette\Utils\Json::encode($this->payload);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Application/Helpers.php:
--------------------------------------------------------------------------------
1 | $class] + class_parents($class);
42 | $addTraits = function (string $type) use (&$res, &$addTraits): void {
43 | $res += class_uses($type);
44 | foreach (class_uses($type) as $trait) {
45 | $addTraits($trait);
46 | }
47 | };
48 | foreach ($res as $type) {
49 | $addTraits($type);
50 | }
51 |
52 | return $res;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationTracy/dist/tab.phtml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
11 | no route
12 | = Tracy\Helpers::escapeHtml($matched[Presenter::PresenterKey]) ?>
13 | := Tracy\Helpers::escapeHtml($matched[Presenter::ActionKey] ?? Presenter::DefaultAction) ?>
14 |
15 |
16 | = Tracy\Helpers::escapeHtml($matched[Presenter::SignalKey]) ?>
17 | !
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Application/exceptions.php:
--------------------------------------------------------------------------------
1 | code, $previous);
58 | }
59 |
60 |
61 | public function getHttpCode(): int
62 | {
63 | return $this->code;
64 | }
65 | }
66 |
67 |
68 | /**
69 | * Access to the requested resource is forbidden.
70 | */
71 | class ForbiddenRequestException extends BadRequestException
72 | {
73 | /** @var int */
74 | protected $code = Http\IResponse::S403_Forbidden;
75 | }
76 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationDI/PresenterFactoryCallback.php:
--------------------------------------------------------------------------------
1 | container->findByType($class);
32 | if (count($services) > 1) {
33 | $services = array_values(array_filter($services, fn($service) => $this->container->getServiceType($service) === $class));
34 | if (count($services) > 1) {
35 | throw new Nette\Application\InvalidPresenterException("Multiple services of type $class found: " . implode(', ', $services) . '.');
36 | }
37 | }
38 |
39 | if (count($services) === 1) {
40 | return $this->container->createService($services[0]);
41 | }
42 |
43 | if ($this->touchToRefresh && class_exists($class)) {
44 | touch($this->touchToRefresh);
45 | 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.';
46 | header('Refresh: 5');
47 | exit;
48 | }
49 |
50 | throw new Nette\Application\InvalidPresenterException("No services of type $class found.");
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Application/UI/Link.php:
--------------------------------------------------------------------------------
1 | component;
33 | }
34 |
35 |
36 | /**
37 | * Returns link destination.
38 | */
39 | public function getDestination(): string
40 | {
41 | return $this->destination;
42 | }
43 |
44 |
45 | /**
46 | * Changes link parameter.
47 | */
48 | public function setParameter(string $key, mixed $value): static
49 | {
50 | $this->params[$key] = $value;
51 | return $this;
52 | }
53 |
54 |
55 | /**
56 | * Returns link parameter.
57 | */
58 | public function getParameter(string $key): mixed
59 | {
60 | return $this->params[$key] ?? null;
61 | }
62 |
63 |
64 | /**
65 | * Returns link parameters.
66 | */
67 | public function getParameters(): array
68 | {
69 | return $this->params;
70 | }
71 |
72 |
73 | /**
74 | * Determines whether this links to the current page.
75 | */
76 | public function isLinkCurrent(): bool
77 | {
78 | return $this->component->isLinkCurrent($this->destination, $this->params);
79 | }
80 |
81 |
82 | /**
83 | * Converts link to URL.
84 | */
85 | public function __toString(): string
86 | {
87 | return $this->component->link($this->destination, $this->params);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nette/application",
3 | "description": "🏆 Nette Application: a full-stack component-based MVC kernel for PHP that helps you write powerful and modern web applications. Write less, have cleaner code and your work will bring you joy.",
4 | "keywords": ["nette", "mvc", "framework", "component-based", "routing", "seo", "mvp", "presenter", "control", "forms"],
5 | "homepage": "https://nette.org",
6 | "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
7 | "authors": [
8 | {
9 | "name": "David Grudl",
10 | "homepage": "https://davidgrudl.com"
11 | },
12 | {
13 | "name": "Nette Community",
14 | "homepage": "https://nette.org/contributors"
15 | }
16 | ],
17 | "require": {
18 | "php": "8.2 - 8.5",
19 | "nette/component-model": "^3.2",
20 | "nette/http": "^3.3.2",
21 | "nette/routing": "^3.1.1",
22 | "nette/utils": "^4.1"
23 | },
24 | "suggest": {
25 | "nette/forms": "Allows to use Nette\\Application\\UI\\Form",
26 | "latte/latte": "Allows using Latte in templates"
27 | },
28 | "require-dev": {
29 | "nette/tester": "^2.5.2",
30 | "nette/di": "^3.2",
31 | "nette/forms": "^3.2",
32 | "nette/robot-loader": "^4.0",
33 | "nette/security": "^3.2",
34 | "latte/latte": "^3.1",
35 | "tracy/tracy": "^2.11",
36 | "mockery/mockery": "1.6.x-dev",
37 | "phpstan/phpstan-nette": "^2.0@stable",
38 | "jetbrains/phpstorm-attributes": "^1.2"
39 | },
40 | "conflict": {
41 | "nette/caching": "<3.2",
42 | "nette/di": "<3.2",
43 | "nette/forms": "<3.2",
44 | "nette/schema": "<1.3",
45 | "latte/latte": "<3.1 || >=3.2",
46 | "tracy/tracy": "<2.11"
47 | },
48 | "autoload": {
49 | "classmap": ["src/"],
50 | "psr-4": {
51 | "Nette\\": "src"
52 | }
53 | },
54 | "minimum-stability": "dev",
55 | "scripts": {
56 | "phpstan": "phpstan analyse",
57 | "tester": "tester tests -s"
58 | },
59 | "extra": {
60 | "branch-alias": {
61 | "dev-master": "4.0-dev"
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/Nodes/IfCurrentNode.php:
--------------------------------------------------------------------------------
1 | position->line})", E_USER_DEPRECATED);
34 | $node = $tag->node = new static;
35 | if (!$tag->parser->isEnd()) {
36 | $node->destination = $tag->parser->parseUnquotedStringOrExpression();
37 | $tag->parser->stream->tryConsume(',');
38 | $node->args = $tag->parser->parseArguments();
39 | }
40 |
41 | [$node->content] = yield;
42 | return $node;
43 | }
44 |
45 |
46 | public function print(PrintContext $context): string
47 | {
48 | return $this->destination
49 | ? $context->format(
50 | 'if ($this->global->uiPresenter->isLinkCurrent(%node, %args?)) { %node } ',
51 | $this->destination,
52 | $this->args,
53 | $this->content,
54 | )
55 | : $context->format(
56 | 'if ($this->global->uiPresenter->getLastCreatedRequestFlag("current")) { %node } ',
57 | $this->content,
58 | );
59 | }
60 |
61 |
62 | public function &getIterator(): \Generator
63 | {
64 | if ($this->destination) {
65 | yield $this->destination;
66 | yield $this->args;
67 | }
68 | yield $this->content;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/compatibility-intf.php:
--------------------------------------------------------------------------------
1 | expectArguments();
37 | if (!$tag->isInHead()) {
38 | throw new CompileException("{{$tag->name}} must be placed in template head.", $tag->position);
39 | }
40 |
41 | $node = new static;
42 | $node->base = $tag->parser->parseUnquotedStringOrExpression();
43 | return $node;
44 | }
45 |
46 |
47 | public function print(PrintContext $context): string
48 | {
49 | return '';
50 | }
51 |
52 |
53 | public function &getIterator(): \Generator
54 | {
55 | yield $this->base;
56 | }
57 |
58 |
59 | public static function applyLinkBasePass(TemplateNode $node): void
60 | {
61 | $base = NodeHelpers::findFirst($node, fn(Node $node) => $node instanceof self)?->base;
62 | if ($base === null) {
63 | return;
64 | }
65 |
66 | (new NodeTraverser)->traverse($node, function (Node $link) use ($base) {
67 | if ($link instanceof LinkNode) {
68 | if ($link->destination instanceof StringNode && $base instanceof StringNode) {
69 | $link->destination->value = LinkGenerator::applyBase($link->destination->value, $base->value);
70 | } else {
71 | $origDestination = $link->destination;
72 | $link->destination = new AuxiliaryNode(
73 | fn(PrintContext $context) => $context->format(
74 | LinkGenerator::class . '::applyBase(%node, %node)',
75 | $origDestination,
76 | $base,
77 | ),
78 | );
79 | }
80 | }
81 | });
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Application/templates/error.phtml:
--------------------------------------------------------------------------------
1 | ['Oops...', 'Your browser sent a request that this server could not understand or process.'],
14 | 403 => ['Access Denied', 'You do not have permission to view this page. Please try contact the web site administrator if you believe you should be able to view this page.'],
15 | 404 => ['Page Not Found', 'The page you requested could not be found. It is possible that the address is incorrect, or that the page no longer exists. Please use a search engine to find what you are looking for.'],
16 | 405 => ['Method Not Allowed', 'The requested method is not allowed for the URL.'],
17 | 410 => ['Page Not Found', 'The page you requested has been taken off the site. We apologize for the inconvenience.'],
18 | 500 => ['Server Error', 'We\'re sorry! The server encountered an internal error and was unable to complete your request. Please try again later.'],
19 | ];
20 | $message = $messages[$code] ?? $messages[0];
21 |
22 | ?>
23 |
24 |
25 |
26 |
27 |
= $message[0] ?>
28 |
29 |
37 |
38 |
39 |
40 |
= $message[0] ?>
41 |
42 |
= $message[1] ?>
43 |
44 |
error = $code ?>
45 |
46 |
47 |
48 |
51 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/Nodes/SnippetAreaNode.php:
--------------------------------------------------------------------------------
1 | */
35 | public static function create(Tag $tag, TemplateParser $parser): \Generator
36 | {
37 | $node = $tag->node = new static;
38 | $name = $tag->parser->parseUnquotedStringOrExpression();
39 | if (
40 | $name instanceof Expression\ClassConstantFetchNode
41 | && $name->class instanceof Php\NameNode
42 | && $name->name instanceof Php\IdentifierNode
43 | ) {
44 | $name = new Scalar\StringNode(constant($name->class . '::' . $name->name), $name->position);
45 | }
46 | $node->block = new Block($name, Template::LayerSnippet, $tag);
47 | $parser->checkBlockIsUnique($node->block);
48 | [$node->content, $endTag] = yield;
49 | if ($endTag && $name instanceof Scalar\StringNode) {
50 | $endTag->parser->stream->tryConsume($name->value);
51 | }
52 | return $node;
53 | }
54 |
55 |
56 | public function print(PrintContext $context): string
57 | {
58 | $context->addBlock($this->block);
59 | $this->block->content = $context->format(
60 | <<<'XX'
61 | $this->global->snippetDriver->enter(%node, %dump);
62 | try {
63 | %node
64 | } finally {
65 | $this->global->snippetDriver->leave();
66 | }
67 |
68 | XX,
69 | $this->block->name,
70 | SnippetRuntime::TypeArea,
71 | $this->content,
72 | );
73 |
74 | return $context->format(
75 | '$this->renderBlock(%node, [], null, %dump) %line;',
76 | $this->block->name,
77 | Template::LayerSnippet,
78 | $this->position,
79 | );
80 | }
81 |
82 |
83 | public function &getIterator(): \Generator
84 | {
85 | yield $this->block->name;
86 | yield $this->content;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/Nodes/LinkNode.php:
--------------------------------------------------------------------------------
1 | outputMode = $tag::OutputKeepIndentation;
36 | $tag->expectArguments();
37 | $node = new static;
38 | $node->destination = $tag->parser->parseUnquotedStringOrExpression();
39 | $tag->parser->stream->tryConsume(',');
40 | $node->args = $tag->parser->parseArguments();
41 | $node->modifier = $tag->parser->parseModifier();
42 | $node->modifier->escape = true;
43 | $node->mode = $tag->name;
44 |
45 | if ($tag->isNAttribute()) {
46 | // move at the beginning
47 | $node->position = $tag->position;
48 | array_unshift($tag->htmlElement->attributes->children, $node);
49 | return null;
50 | }
51 |
52 | return $node;
53 | }
54 |
55 |
56 | public function print(PrintContext $context): string
57 | {
58 | if ($this->mode === 'href') {
59 | $context->beginEscape()->enterHtmlAttribute(null);
60 | $res = $context->format(
61 | <<<'XX'
62 | echo ' href="'; echo %modify($this->global->uiControl->link(%node, %node?)) %line; echo '"';
63 | XX,
64 | $this->modifier,
65 | $this->destination,
66 | $this->args,
67 | $this->position,
68 | );
69 | $context->restoreEscape();
70 | return $res;
71 | }
72 |
73 | return $context->format(
74 | 'echo %modify('
75 | . ($this->mode === 'plink' ? '$this->global->uiPresenter' : '$this->global->uiControl')
76 | . '->link(%node, %node?)) %line;',
77 | $this->modifier,
78 | $this->destination,
79 | $this->args,
80 | $this->position,
81 | );
82 | }
83 |
84 |
85 | public function &getIterator(): \Generator
86 | {
87 | yield $this->destination;
88 | yield $this->args;
89 | yield $this->modifier;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Application/Routers/CliRouter.php:
--------------------------------------------------------------------------------
1 | defaults = $defaults;
29 | }
30 |
31 |
32 | /**
33 | * Maps command line arguments to an array.
34 | */
35 | public function match(Nette\Http\IRequest $httpRequest): ?array
36 | {
37 | if (empty($_SERVER['argv']) || !is_array($_SERVER['argv'])) {
38 | return null;
39 | }
40 |
41 | $names = [self::PresenterKey];
42 | $params = $this->defaults;
43 | $args = $_SERVER['argv'];
44 | array_shift($args);
45 | $args[] = '--';
46 |
47 | foreach ($args as $arg) {
48 | $opt = preg_replace('#/|-+#A', '', $arg);
49 | if ($opt === $arg) {
50 | if (isset($flag) || $flag = array_shift($names)) {
51 | $params[$flag] = $arg;
52 | } else {
53 | $params[] = $arg;
54 | }
55 |
56 | $flag = null;
57 | continue;
58 | }
59 |
60 | if (isset($flag)) {
61 | $params[$flag] = true;
62 | $flag = null;
63 | }
64 |
65 | if ($opt === '') {
66 | continue;
67 | }
68 |
69 | $pair = explode('=', $opt, 2);
70 | if (isset($pair[1])) {
71 | $params[$pair[0]] = $pair[1];
72 | } else {
73 | $flag = $pair[0];
74 | }
75 | }
76 |
77 | if (!isset($params[self::PresenterKey])) {
78 | throw new Nette\InvalidStateException('Missing presenter & action in route definition.');
79 | }
80 |
81 | [$module, $presenter] = Nette\Application\Helpers::splitName($params[self::PresenterKey]);
82 | if ($module !== '') {
83 | $params[self::PresenterKey] = $presenter;
84 | $presenter = $module;
85 | }
86 |
87 | $params['presenter'] = $presenter;
88 |
89 | return $params;
90 | }
91 |
92 |
93 | /**
94 | * This router is only unidirectional.
95 | */
96 | public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string
97 | {
98 | return null;
99 | }
100 |
101 |
102 | /**
103 | * Returns default values.
104 | */
105 | public function getDefaults(): array
106 | {
107 | return $this->defaults;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | Licenses
2 | ========
3 |
4 | Good news! You may use Nette Framework under the terms of either
5 | the New BSD License or the GNU General Public License (GPL) version 2 or 3.
6 |
7 | The BSD License is recommended for most projects. It is easy to understand and it
8 | places almost no restrictions on what you can do with the framework. If the GPL
9 | fits better to your project, you can use the framework under this license.
10 |
11 | You don't have to notify anyone which license you are using. You can freely
12 | use Nette Framework in commercial projects as long as the copyright header
13 | remains intact.
14 |
15 | Please be advised that the name "Nette Framework" is a protected trademark and its
16 | usage has some limitations. So please do not use word "Nette" in the name of your
17 | project or top-level domain, and choose a name that stands on its own merits.
18 | If your stuff is good, it will not take long to establish a reputation for yourselves.
19 |
20 |
21 | New BSD License
22 | ---------------
23 |
24 | Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
25 | All rights reserved.
26 |
27 | Redistribution and use in source and binary forms, with or without modification,
28 | are permitted provided that the following conditions are met:
29 |
30 | * Redistributions of source code must retain the above copyright notice,
31 | this list of conditions and the following disclaimer.
32 |
33 | * Redistributions in binary form must reproduce the above copyright notice,
34 | this list of conditions and the following disclaimer in the documentation
35 | and/or other materials provided with the distribution.
36 |
37 | * Neither the name of "Nette Framework" nor the names of its contributors
38 | may be used to endorse or promote products derived from this software
39 | without specific prior written permission.
40 |
41 | This software is provided by the copyright holders and contributors "as is" and
42 | any express or implied warranties, including, but not limited to, the implied
43 | warranties of merchantability and fitness for a particular purpose are
44 | disclaimed. In no event shall the copyright owner or contributors be liable for
45 | any direct, indirect, incidental, special, exemplary, or consequential damages
46 | (including, but not limited to, procurement of substitute goods or services;
47 | loss of use, data, or profits; or business interruption) however caused and on
48 | any theory of liability, whether in contract, strict liability, or tort
49 | (including negligence or otherwise) arising in any way out of the use of this
50 | software, even if advised of the possibility of such damage.
51 |
52 |
53 | GNU General Public License
54 | --------------------------
55 |
56 | GPL licenses are very very long, so instead of including them here we offer
57 | you URLs with full text:
58 |
59 | - [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
60 | - [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
61 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/TemplateFactory.php:
--------------------------------------------------------------------------------
1 | Occurs when a new template is created */
23 | public array $onCreate = [];
24 | private string $templateClass;
25 |
26 |
27 | public function __construct(
28 | private readonly LatteFactory $latteFactory,
29 | private readonly ?Nette\Http\IRequest $httpRequest = null,
30 | private readonly ?Nette\Security\User $user = null,
31 | $templateClass = null,
32 | private bool $generate = false,
33 | ) {
34 | if ($templateClass && (!class_exists($templateClass) || !is_a($templateClass, Template::class, true))) {
35 | throw new Nette\InvalidArgumentException("Class $templateClass does not implement " . Template::class . ' or it does not exist.');
36 | }
37 |
38 | $this->templateClass = $templateClass ?? DefaultTemplate::class;
39 | }
40 |
41 |
42 | public function createTemplate(?UI\Control $control = null, ?string $class = null): Template
43 | {
44 | $class ??= $this->templateClass;
45 | if (!is_a($class, Template::class, allow_string: true)) {
46 | throw new Nette\InvalidArgumentException("Class $class does not implement " . Template::class . ' or it does not exist.');
47 | }
48 |
49 | $latte = $this->latteFactory->create($control);
50 | $template = $this->generate && $control instanceof UI\Presenter
51 | ? new TemplateGenerator($latte, $class, $control)
52 | : new $class($latte);
53 | $this->injectDefaultVariables($template, $control);
54 |
55 | Nette\Utils\Arrays::invoke($this->onCreate, $template);
56 |
57 | return $template;
58 | }
59 |
60 |
61 | private function injectDefaultVariables(Template $template, ?UI\Control $control): void
62 | {
63 | $presenter = $control?->getPresenterIfExists();
64 | $baseUrl = $this->httpRequest
65 | ? rtrim($this->httpRequest->getUrl()->withoutUserInfo()->getBaseUrl(), '/')
66 | : null;
67 | $flashes = $presenter instanceof UI\Presenter && $presenter->hasFlashSession()
68 | ? (array) $presenter->getFlashSession()->get($control->getParameterId('flash'))
69 | : [];
70 |
71 | $vars = [
72 | 'user' => $this->user,
73 | 'baseUrl' => $baseUrl,
74 | 'basePath' => $baseUrl ? preg_replace('#https?://[^/]+#A', '', $baseUrl) : null,
75 | 'flashes' => $flashes,
76 | 'control' => $control,
77 | 'presenter' => $presenter,
78 | ];
79 |
80 | foreach ($vars as $key => $value) {
81 | if ($value !== null && property_exists($template, $key)) {
82 | try {
83 | $template->$key = $value;
84 | } catch (\TypeError) {
85 | }
86 | } elseif ($template instanceof TemplateGenerator) {
87 | $template->addDefaultVariable($key, $value);
88 | }
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationDI/RoutingExtension.php:
--------------------------------------------------------------------------------
1 | Expect::bool(),
33 | 'routes' => Expect::arrayOf('string'),
34 | 'cache' => Expect::bool(false),
35 | ]);
36 | }
37 |
38 |
39 | public function loadConfiguration(): void
40 | {
41 | if (!$this->config->routes) {
42 | return;
43 | }
44 |
45 | $builder = $this->getContainerBuilder();
46 |
47 | $router = $builder->addDefinition($this->prefix('router'))
48 | ->setFactory(Nette\Application\Routers\RouteList::class);
49 |
50 | foreach ($this->config->routes as $mask => $action) {
51 | $router->addSetup('$service->addRoute(?, ?)', [$mask, $action]);
52 | }
53 |
54 | if ($this->name === 'routing') {
55 | $builder->addAlias('router', $this->prefix('router'));
56 | }
57 | }
58 |
59 |
60 | public function beforeCompile(): void
61 | {
62 | $builder = $this->getContainerBuilder();
63 |
64 | if (
65 | $this->debugMode &&
66 | ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class)) &&
67 | ($name = $builder->getByType(Nette\Application\Application::class)) &&
68 | ($application = $builder->getDefinition($name)) instanceof Definitions\ServiceDefinition
69 | ) {
70 | $application->addSetup('@Tracy\Bar::addPanel', [
71 | new Definitions\Statement(Nette\Bridges\ApplicationTracy\RoutingPanel::class),
72 | ]);
73 | }
74 |
75 | if (!$builder->getByType(Nette\Routing\Router::class)) {
76 | $builder->addDefinition($this->prefix('router'))
77 | ->setType(Nette\Routing\Router::class)
78 | ->setFactory(Nette\Routing\SimpleRouter::class);
79 | $builder->addAlias('router', $this->prefix('router'));
80 | }
81 | }
82 |
83 |
84 | public function afterCompile(Nette\PhpGenerator\ClassType $class): void
85 | {
86 | if ($this->config->cache) {
87 | $builder = $this->getContainerBuilder();
88 | $def = $builder->getDefinitionByType(Nette\Routing\Router::class);
89 | $method = $class->getMethod(Nette\DI\Container::getMethodName($def->getName()));
90 | try {
91 | $router = eval($method->getBody());
92 | if ($router instanceof Nette\Application\Routers\RouteList) {
93 | $router->warmupCache();
94 | }
95 |
96 | $s = serialize($router);
97 | } catch (\Throwable $e) {
98 | throw new Nette\DI\ServiceCreationException('Unable to cache router due to error: ' . $e->getMessage(), 0, $e);
99 | }
100 |
101 | $method->setBody('return unserialize(?);', [$s]);
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 | '@']));
67 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/Template.php:
--------------------------------------------------------------------------------
1 | latte;
33 | }
34 |
35 |
36 | /**
37 | * Renders template to output.
38 | */
39 | public function render(?string $file = null, array $params = []): void
40 | {
41 | Nette\Utils\Arrays::toObject($params, $this);
42 | $this->latte->render($file ?? $this->file, $this);
43 | }
44 |
45 |
46 | /**
47 | * Renders template to output.
48 | */
49 | public function renderToString(?string $file = null, array $params = []): string
50 | {
51 | Nette\Utils\Arrays::toObject($params, $this);
52 | return $this->latte->renderToString($file ?? $this->file, $this);
53 | }
54 |
55 |
56 | /**
57 | * Renders template to string.
58 | */
59 | public function __toString(): string
60 | {
61 | return $this->latte->renderToString($this->file, $this->getParameters());
62 | }
63 |
64 |
65 | /********************* template filters & helpers ****************d*g**/
66 |
67 |
68 | /**
69 | * Registers run-time filter.
70 | */
71 | public function addFilter(?string $name, callable $callback): static
72 | {
73 | $this->latte->addFilter($name, $callback);
74 | return $this;
75 | }
76 |
77 |
78 | /**
79 | * Registers run-time function.
80 | */
81 | public function addFunction(string $name, callable $callback): static
82 | {
83 | $this->latte->addFunction($name, $callback);
84 | return $this;
85 | }
86 |
87 |
88 | /**
89 | * Sets translate adapter.
90 | */
91 | public function setTranslator(?Nette\Localization\Translator $translator, ?string $language = null): static
92 | {
93 | $this->latte->addExtension(new Latte\Essential\TranslatorExtension($translator, $language));
94 | return $this;
95 | }
96 |
97 |
98 | /********************* template parameters ****************d*g**/
99 |
100 |
101 | /**
102 | * Sets the path to the template file.
103 | */
104 | public function setFile(string $file): static
105 | {
106 | $this->file = $file;
107 | return $this;
108 | }
109 |
110 |
111 | final public function getFile(): ?string
112 | {
113 | return $this->file;
114 | }
115 |
116 |
117 | /**
118 | * Returns array of all parameters.
119 | */
120 | public function getParameters(): array
121 | {
122 | $res = [];
123 | foreach ((new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
124 | if ($prop->isInitialized($this)) {
125 | $res[$prop->getName()] = $prop->getValue($this);
126 | }
127 | }
128 |
129 | return $res;
130 | }
131 |
132 |
133 | /**
134 | * Prevents unserialization.
135 | */
136 | final public function __unserialize($_)
137 | {
138 | throw new Nette\NotImplementedException('Object unserialization is not supported by class ' . static::class);
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Application/Responses/FileResponse.php:
--------------------------------------------------------------------------------
1 | file = $file;
39 | $this->name = $name ?? basename($file);
40 | $this->contentType = $contentType ?? 'application/octet-stream';
41 | $this->forceDownload = $forceDownload;
42 | }
43 |
44 |
45 | /**
46 | * Returns the path to a downloaded file.
47 | */
48 | public function getFile(): string
49 | {
50 | return $this->file;
51 | }
52 |
53 |
54 | /**
55 | * Returns the file name.
56 | */
57 | public function getName(): string
58 | {
59 | return $this->name;
60 | }
61 |
62 |
63 | /**
64 | * Returns the MIME content type of a downloaded file.
65 | */
66 | public function getContentType(): string
67 | {
68 | return $this->contentType;
69 | }
70 |
71 |
72 | /**
73 | * Sends response to output.
74 | */
75 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void
76 | {
77 | $httpResponse->setContentType($this->contentType);
78 | $httpResponse->setHeader(
79 | 'Content-Disposition',
80 | ($this->forceDownload ? 'attachment' : 'inline')
81 | . '; filename="' . $this->name . '"'
82 | . '; filename*=utf-8\'\'' . rawurlencode($this->name),
83 | );
84 |
85 | $filesize = $length = filesize($this->file);
86 | $handle = fopen($this->file, 'r');
87 | if (!$handle) {
88 | throw new Nette\Application\BadRequestException("Cannot open file: '{$this->file}'.");
89 | }
90 |
91 | if ($this->resuming) {
92 | $httpResponse->setHeader('Accept-Ranges', 'bytes');
93 | if (preg_match('#^bytes=(\d*)-(\d*)$#D', (string) $httpRequest->getHeader('Range'), $matches)) {
94 | [, $start, $end] = $matches;
95 | if ($start === '') {
96 | $start = max(0, $filesize - $end);
97 | $end = $filesize - 1;
98 |
99 | } elseif ($end === '' || $end > $filesize - 1) {
100 | $end = $filesize - 1;
101 | }
102 |
103 | if ($end < $start) {
104 | $httpResponse->setCode(416); // requested range not satisfiable
105 | return;
106 | }
107 |
108 | $httpResponse->setCode(206);
109 | $httpResponse->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $filesize);
110 | $length = $end - $start + 1;
111 | fseek($handle, (int) $start);
112 |
113 | } else {
114 | $httpResponse->setHeader('Content-Range', 'bytes 0-' . ($filesize - 1) . '/' . $filesize);
115 | }
116 | }
117 |
118 | $httpResponse->setHeader('Content-Length', (string) $length);
119 | while (!feof($handle) && $length > 0) {
120 | echo $s = fread($handle, min(4_000_000, $length));
121 | $length -= strlen($s);
122 | }
123 |
124 | fclose($handle);
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/SnippetRuntime.php:
--------------------------------------------------------------------------------
1 | */
30 | private array $stack = [];
31 | private int $nestingLevel = 0;
32 | private bool $renderingSnippets = false;
33 |
34 | private ?\stdClass $payload;
35 |
36 |
37 | public function __construct(
38 | private readonly Control $control,
39 | ) {
40 | }
41 |
42 |
43 | public function enter(string $name, string $type): void
44 | {
45 | if (!$this->renderingSnippets) {
46 | if ($type === self::TypeDynamic && $this->nestingLevel === 0) {
47 | trigger_error('Dynamic snippets are allowed only inside static snippet/snippetArea.', E_USER_WARNING);
48 | }
49 |
50 | $this->nestingLevel++;
51 | return;
52 | }
53 |
54 | $obStarted = false;
55 | if (
56 | ($this->nestingLevel === 0 && $this->control->isControlInvalid($name))
57 | || ($type === self::TypeDynamic && ($previous = end($this->stack)) && $previous[1] === true)
58 | ) {
59 | ob_start(fn() => '');
60 | $this->nestingLevel = $type === self::TypeArea ? 0 : 1;
61 | $obStarted = true;
62 | } elseif ($this->nestingLevel > 0) {
63 | $this->nestingLevel++;
64 | }
65 |
66 | $this->stack[] = [$name, $obStarted];
67 | if ($name !== '') {
68 | $this->control->redrawControl($name, false);
69 | }
70 | }
71 |
72 |
73 | public function leave(): void
74 | {
75 | if (!$this->renderingSnippets) {
76 | $this->nestingLevel--;
77 | return;
78 | }
79 |
80 | [$name, $obStarted] = array_pop($this->stack);
81 | if ($this->nestingLevel > 0 && --$this->nestingLevel === 0) {
82 | $content = ob_get_clean();
83 | $this->payload ??= $this->control->getPresenter()->getPayload();
84 | $this->payload->snippets[$this->control->getSnippetId($name)] = $content;
85 |
86 | } elseif ($obStarted) { // dynamic snippet wrapper or snippet area
87 | ob_end_clean();
88 | }
89 | }
90 |
91 |
92 | public function getHtmlId(string $name): string
93 | {
94 | return $this->control->getSnippetId($name);
95 | }
96 |
97 |
98 | /**
99 | * @param Block[] $blocks
100 | * @param mixed[] $params
101 | */
102 | public function renderSnippets(array $blocks, array $params): bool
103 | {
104 | if ($this->renderingSnippets || !$this->control->snippetMode) {
105 | return false;
106 | }
107 |
108 | $this->renderingSnippets = true;
109 | $this->control->snippetMode = false;
110 | foreach ($blocks as $name => $block) {
111 | if (!$this->control->isControlInvalid($name)) {
112 | continue;
113 | }
114 |
115 | $function = reset($block->functions);
116 | $function($params);
117 | }
118 |
119 | $this->control->snippetMode = true;
120 | $this->renderChildren();
121 |
122 | $this->renderingSnippets = false;
123 | return true;
124 | }
125 |
126 |
127 | private function renderChildren(): void
128 | {
129 | $queue = [$this->control];
130 | do {
131 | foreach (array_shift($queue)->getComponents() as $child) {
132 | if ($child instanceof Renderable) {
133 | if ($child->isControlInvalid()) {
134 | $child->snippetMode = true;
135 | $child->render();
136 | $child->snippetMode = false;
137 | }
138 | } elseif ($child instanceof Nette\ComponentModel\IContainer) {
139 | $queue[] = $child;
140 | }
141 | }
142 | } while ($queue);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/Application/Request.php:
--------------------------------------------------------------------------------
1 | name = $name;
56 | return $this;
57 | }
58 |
59 |
60 | /**
61 | * Retrieve the presenter name.
62 | */
63 | public function getPresenterName(): string
64 | {
65 | return $this->name;
66 | }
67 |
68 |
69 | /**
70 | * Sets variables provided to the presenter.
71 | */
72 | public function setParameters(array $params): static
73 | {
74 | $this->params = $params;
75 | return $this;
76 | }
77 |
78 |
79 | /**
80 | * Returns all variables provided to the presenter (usually via URL).
81 | */
82 | public function getParameters(): array
83 | {
84 | return $this->params;
85 | }
86 |
87 |
88 | /**
89 | * Returns a parameter provided to the presenter.
90 | */
91 | public function getParameter(string $key): mixed
92 | {
93 | return $this->params[$key] ?? null;
94 | }
95 |
96 |
97 | /**
98 | * Sets variables provided to the presenter via POST.
99 | */
100 | public function setPost(array $params): static
101 | {
102 | $this->post = $params;
103 | return $this;
104 | }
105 |
106 |
107 | /**
108 | * Returns a variable provided to the presenter via POST.
109 | * If no key is passed, returns the entire array.
110 | */
111 | public function getPost(?string $key = null): mixed
112 | {
113 | return func_num_args() === 0
114 | ? $this->post
115 | : ($this->post[$key] ?? null);
116 | }
117 |
118 |
119 | /**
120 | * Sets all uploaded files.
121 | */
122 | public function setFiles(array $files): static
123 | {
124 | $this->files = $files;
125 | return $this;
126 | }
127 |
128 |
129 | /**
130 | * Returns all uploaded files.
131 | */
132 | public function getFiles(): array
133 | {
134 | return $this->files;
135 | }
136 |
137 |
138 | /**
139 | * Sets the method.
140 | */
141 | public function setMethod(?string $method): static
142 | {
143 | $this->method = $method;
144 | return $this;
145 | }
146 |
147 |
148 | /**
149 | * Returns the method.
150 | */
151 | public function getMethod(): ?string
152 | {
153 | return $this->method;
154 | }
155 |
156 |
157 | /**
158 | * Checks if the method is the given one.
159 | */
160 | public function isMethod(string $method): bool
161 | {
162 | return strcasecmp($this->method ?? '', $method) === 0;
163 | }
164 |
165 |
166 | /**
167 | * Sets the flag.
168 | */
169 | public function setFlag(string $flag, bool $value = true): static
170 | {
171 | $this->flags[$flag] = $value;
172 | return $this;
173 | }
174 |
175 |
176 | /**
177 | * Checks the flag.
178 | */
179 | public function hasFlag(string $flag): bool
180 | {
181 | return !empty($this->flags[$flag]);
182 | }
183 |
184 |
185 | public function toArray(): array
186 | {
187 | $params = $this->params;
188 | $params['presenter'] = $this->name;
189 | return $params;
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/Application/Routers/RouteList.php:
--------------------------------------------------------------------------------
1 | module = $module ? $module . ':' : null;
31 | }
32 |
33 |
34 | /**
35 | * Support for modules.
36 | */
37 | protected function completeParameters(array $params): ?array
38 | {
39 | $presenter = $params[self::PresenterKey] ?? null;
40 | if (is_string($presenter) && strncmp($presenter, 'Nette:', 6)) {
41 | $params[self::PresenterKey] = $this->module . $presenter;
42 | }
43 |
44 | return $params;
45 | }
46 |
47 |
48 | /**
49 | * Constructs absolute URL from array.
50 | */
51 | public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string
52 | {
53 | if ($this->module) {
54 | if (strncmp($params[self::PresenterKey], $this->module, strlen($this->module)) !== 0) {
55 | return null;
56 | }
57 |
58 | $params[self::PresenterKey] = substr($params[self::PresenterKey], strlen($this->module));
59 | }
60 |
61 | return parent::constructUrl($params, $refUrl);
62 | }
63 |
64 |
65 | public function addRoute(
66 | #[Language('TEXT')]
67 | string $mask,
68 | array|string|\Closure $metadata = [],
69 | bool $oneWay = false,
70 | ): static
71 | {
72 | $this->add(new Route($mask, $metadata), $oneWay);
73 | return $this;
74 | }
75 |
76 |
77 | public function withModule(string $module): static
78 | {
79 | $router = new static;
80 | $router->module = $module . ':';
81 | $router->parent = $this;
82 | $this->add($router);
83 | return $router;
84 | }
85 |
86 |
87 | public function getModule(): ?string
88 | {
89 | return $this->module;
90 | }
91 |
92 |
93 | /**
94 | * @param mixed $index
95 | * @param Nette\Routing\Router $router
96 | */
97 | public function offsetSet($index, $router): void
98 | {
99 | if ($router instanceof Route) {
100 | trigger_error('Usage `$router[] = new Route(...)` is deprecated, use `$router->addRoute(...)`.', E_USER_DEPRECATED);
101 | } else {
102 | $class = getclass($router);
103 | trigger_error("Usage `\$router[] = new $class` is deprecated, use `\$router->add(new $class)`.", E_USER_DEPRECATED);
104 | }
105 |
106 | if ($index === null) {
107 | $this->add($router);
108 | } else {
109 | $this->modify($index, $router);
110 | }
111 | }
112 |
113 |
114 | /**
115 | * @param int $index
116 | * @throws Nette\OutOfRangeException
117 | */
118 | public function offsetGet($index): Nette\Routing\Router
119 | {
120 | trigger_error('Usage `$route = $router[...]` is deprecated, use `$router->getRouters()`.', E_USER_DEPRECATED);
121 | if (!$this->offsetExists($index)) {
122 | throw new Nette\OutOfRangeException('Offset invalid or out of range');
123 | }
124 |
125 | return $this->getRouters()[$index];
126 | }
127 |
128 |
129 | /**
130 | * @param int $index
131 | */
132 | public function offsetExists($index): bool
133 | {
134 | trigger_error('Usage `isset($router[...])` is deprecated.', E_USER_DEPRECATED);
135 | return is_int($index) && $index >= 0 && $index < count($this->getRouters());
136 | }
137 |
138 |
139 | /**
140 | * @param int $index
141 | * @throws Nette\OutOfRangeException
142 | */
143 | public function offsetUnset($index): void
144 | {
145 | trigger_error('Usage `unset($router[$index])` is deprecated, use `$router->modify($index, null)`.', E_USER_DEPRECATED);
146 | if (!$this->offsetExists($index)) {
147 | throw new Nette\OutOfRangeException('Offset invalid or out of range');
148 | }
149 |
150 | $this->modify($index, null);
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/Nodes/ControlNode.php:
--------------------------------------------------------------------------------
1 | outputMode = $tag::OutputRemoveIndentation;
39 | $tag->expectArguments();
40 | $stream = $tag->parser->stream;
41 | $node = new static;
42 | $node->name = $tag->parser->parseUnquotedStringOrExpression(colon: false);
43 | if ($stream->tryConsume(':')) {
44 | $node->method = $tag->parser->parseExpression();
45 | }
46 |
47 | $stream->tryConsume(',');
48 | $start = $stream->getIndex();
49 | $node->args = $tag->parser->parseArguments();
50 | $start -= $stream->getIndex();
51 | $depth = $wrap = null;
52 | for (; $start < 0; $start++) {
53 | $token = $stream->peek($start);
54 | match (true) {
55 | $token->is('[') => $depth++,
56 | $token->is(']') => $depth--,
57 | $token->is('=>') && !$depth => $wrap = true,
58 | default => null,
59 | };
60 | }
61 |
62 | if ($wrap) {
63 | $node->args = new ArrayNode([new ArrayItemNode($node->args)]);
64 | }
65 |
66 | $modifier = $tag->parser->parseModifier();
67 | $node->escape = $modifier->removeFilter('noescape') ? false : null;
68 | if ($modifier->filters) {
69 | throw new Latte\CompileException('Only modifier |noescape is allowed here.', reset($modifier->filters)->position);
70 | }
71 |
72 | return $node;
73 | }
74 |
75 |
76 | public function print(PrintContext $context): string
77 | {
78 | if ($this->escape === null && $context->getEscaper()->getState() !== Escaper::HtmlText) {
79 | $this->escape = true;
80 | }
81 |
82 | $method = match (true) {
83 | !$this->method => 'render',
84 | $this->method instanceof StringNode && Strings::match($this->method->value, '#^\w*$#D') => 'render' . ucfirst($this->method->value),
85 | default => "{'render' . " . $this->method->print($context) . '}',
86 | };
87 |
88 | $fetchCode = $context->format(
89 | $this->name instanceof StringNode
90 | ? '$ʟ_tmp = $this->global->uiControl->getComponent(%node);'
91 | : 'if (!is_object($ʟ_tmp = %node)) $ʟ_tmp = $this->global->uiControl->getComponent($ʟ_tmp);',
92 | $this->name,
93 | );
94 |
95 | if ($this->escape) {
96 | return $context->format(
97 | <<<'XX'
98 | %raw
99 | if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false);
100 | ob_start(fn() => '');
101 | $ʟ_tmp->%raw(%args) %line;
102 | $ʟ_fi = new LR\FilterInfo(%dump); echo %modifyContent(ob_get_clean());
103 |
104 |
105 | XX,
106 | $fetchCode,
107 | $method,
108 | $this->args,
109 | $this->position,
110 | Latte\ContentType::Html,
111 | new ModifierNode([], $this->escape),
112 | );
113 |
114 | } else {
115 | return $context->format(
116 | <<<'XX'
117 | %raw
118 | if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false);
119 | $ʟ_tmp->%raw(%args) %line;
120 |
121 |
122 | XX,
123 | $fetchCode,
124 | $method,
125 | $this->args,
126 | $this->position,
127 | );
128 | }
129 | }
130 |
131 |
132 | public function &getIterator(): \Generator
133 | {
134 | yield $this->name;
135 | if ($this->method) {
136 | yield $this->method;
137 | }
138 | yield $this->args;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Application/PresenterFactory.php:
--------------------------------------------------------------------------------
1 | splited mask */
22 | private array $mapping = [
23 | '*' => ['App\Presentation\\', '*\\', '**Presenter'],
24 | 'Nette' => ['NetteModule\\', '*\\', '*Presenter'],
25 | ];
26 |
27 | private array $aliases = [];
28 | private array $cache = [];
29 |
30 | /** @var callable */
31 | private $factory;
32 |
33 |
34 | /**
35 | * @param ?callable(string): IPresenter $factory
36 | */
37 | public function __construct(?callable $factory = null)
38 | {
39 | $this->factory = $factory ?? fn(string $class): IPresenter => new $class;
40 | }
41 |
42 |
43 | /**
44 | * Creates new presenter instance.
45 | */
46 | public function createPresenter(string $name): IPresenter
47 | {
48 | return ($this->factory)($this->getPresenterClass($name));
49 | }
50 |
51 |
52 | /**
53 | * Generates and checks presenter class name.
54 | * @throws InvalidPresenterException
55 | */
56 | public function getPresenterClass(string &$name): string
57 | {
58 | if (isset($this->cache[$name])) {
59 | return $this->cache[$name];
60 | }
61 |
62 | $class = $this->formatPresenterClass($name);
63 | if (!class_exists($class)) {
64 | throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' was not found.");
65 | }
66 |
67 | $reflection = new \ReflectionClass($class);
68 | $class = $reflection->getName();
69 | if (!$reflection->implementsInterface(IPresenter::class)) {
70 | throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' is not Nette\\Application\\IPresenter implementor.");
71 | } elseif ($reflection->isAbstract()) {
72 | throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' is abstract.");
73 | }
74 |
75 | return $this->cache[$name] = $class;
76 | }
77 |
78 |
79 | /**
80 | * Sets mapping as pairs [module => mask]
81 | */
82 | public function setMapping(array $mapping): static
83 | {
84 | foreach ($mapping as $module => $mask) {
85 | if (is_string($mask)) {
86 | if (!preg_match('#^\\\?([\w\\\]*\\\)?(\w*\*\w*?\\\)?([\w\\\]*\*\*?\w*)$#D', $mask, $m)) {
87 | throw new Nette\InvalidStateException("Invalid mapping mask '$mask'.");
88 | }
89 |
90 | $this->mapping[$module] = [$m[1], $m[2] ?: '*Module\\', $m[3]];
91 | } elseif (is_array($mask) && count($mask) === 3) {
92 | $this->mapping[$module] = [$mask[0] ? $mask[0] . '\\' : '', $mask[1] . '\\', $mask[2]];
93 | } else {
94 | throw new Nette\InvalidStateException("Invalid mapping mask for module $module.");
95 | }
96 | }
97 |
98 | return $this;
99 | }
100 |
101 |
102 | /**
103 | * Formats presenter class name from its name.
104 | * @internal
105 | */
106 | public function formatPresenterClass(string $presenter): string
107 | {
108 | if (!Nette\Utils\Strings::match($presenter, '#^[a-zA-Z\x7f-\xff][a-zA-Z0-9\x7f-\xff:]*$#D')) {
109 | throw new InvalidPresenterException("Presenter name must be alphanumeric string, '$presenter' is invalid.");
110 | }
111 | $parts = explode(':', $presenter);
112 | $mapping = isset($parts[1], $this->mapping[$parts[0]])
113 | ? $this->mapping[array_shift($parts)]
114 | : $this->mapping['*'];
115 |
116 | while ($part = array_shift($parts)) {
117 | $mapping[0] .= strtr($mapping[$parts ? 1 : 2], ['**' => "$part\\$part", '*' => $part]);
118 | }
119 |
120 | return $mapping[0];
121 | }
122 |
123 |
124 | /**
125 | * Sets pairs [alias => destination]
126 | */
127 | public function setAliases(array $aliases): static
128 | {
129 | $this->aliases = $aliases;
130 | return $this;
131 | }
132 |
133 |
134 | public function getAlias(string $alias): string
135 | {
136 | return $this->aliases[$alias] ?? throw new Nette\InvalidStateException("Link alias '$alias' was not found.");
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/UIExtension.php:
--------------------------------------------------------------------------------
1 | control?->getPresenterIfExists();
36 | return [
37 | 'modifyDate' => fn($time, $delta, $unit = null) => $time
38 | ? Nette\Utils\DateTime::from($time)->modify($delta . $unit)
39 | : null,
40 | ] + ($presenter ? [
41 | 'absoluteUrl' => fn(\Stringable|string|null $link): ?string => $link === null
42 | ? null
43 | : $presenter->getHttpRequest()->getUrl()->resolve((string) $link)->getAbsoluteUrl(),
44 | ] : []);
45 | }
46 |
47 |
48 | public function getFunctions(): array
49 | {
50 | if ($presenter = $this->control?->getPresenterIfExists()) {
51 | return [
52 | 'isLinkCurrent' => $presenter->isLinkCurrent(...),
53 | 'isModuleCurrent' => $presenter->isModuleCurrent(...),
54 | ];
55 | }
56 | return [];
57 | }
58 |
59 |
60 | public function getProviders(): array
61 | {
62 | $presenter = $this->control?->getPresenterIfExists();
63 | $httpResponse = $presenter?->getHttpResponse();
64 | return [
65 | 'coreParentFinder' => $this->findLayoutTemplate(...),
66 | 'uiControl' => $this->control,
67 | 'uiPresenter' => $presenter,
68 | 'snippetDriver' => $this->control ? new SnippetRuntime($this->control) : null,
69 | 'uiNonce' => $httpResponse ? $this->findNonce($httpResponse) : null,
70 | ];
71 | }
72 |
73 |
74 | public function getTags(): array
75 | {
76 | return [
77 | 'n:href' => Nodes\LinkNode::create(...),
78 | 'n:nonce' => Nodes\NNonceNode::create(...),
79 | 'control' => Nodes\ControlNode::create(...),
80 | 'plink' => Nodes\LinkNode::create(...),
81 | 'link' => Nodes\LinkNode::create(...),
82 | 'linkBase' => Nodes\LinkBaseNode::create(...),
83 | 'ifCurrent' => Nodes\IfCurrentNode::create(...),
84 | 'snippet' => Nodes\SnippetNode::create(...),
85 | 'snippetArea' => Nodes\SnippetAreaNode::create(...),
86 | 'layout' => $this->createExtendsNode(...),
87 | 'extends' => $this->createExtendsNode(...),
88 | ];
89 | }
90 |
91 |
92 | public function getPasses(): array
93 | {
94 | return [
95 | 'snippetRendering' => $this->snippetRenderingPass(...),
96 | 'applyLinkBase' => [Nodes\LinkBaseNode::class, 'applyLinkBasePass'],
97 | ];
98 | }
99 |
100 |
101 | /**
102 | * Render snippets instead of template in snippet-mode.
103 | */
104 | public function snippetRenderingPass(TemplateNode $templateNode): void
105 | {
106 | array_unshift($templateNode->main->children, new Latte\Compiler\Nodes\AuxiliaryNode(fn() => <<<'XX'
107 | if ($this->global->snippetDriver?->renderSnippets($this->blocks[self::LayerSnippet], $this->params)) { return; }
108 |
109 |
110 | XX));
111 | }
112 |
113 |
114 | public static function findLayoutTemplate(Latte\Runtime\Template $template): ?string
115 | {
116 | $presenter = $template->global->uiControl ?? null;
117 | return $presenter instanceof UI\Presenter && !empty($template::Blocks[$template::LayerTop])
118 | ? $presenter->findLayoutTemplateFile()
119 | : null;
120 | }
121 |
122 |
123 | private function findNonce(Nette\Http\IResponse $httpResponse): ?string
124 | {
125 | $header = $httpResponse->getHeader('Content-Security-Policy')
126 | ?: $httpResponse->getHeader('Content-Security-Policy-Report-Only');
127 | return preg_match('#\s\'nonce-([\w+/]+=*)\'#', (string) $header, $m) ? $m[1] : null;
128 | }
129 |
130 |
131 | public static function createExtendsNode(Tag $tag): ExtendsNode
132 | {
133 | $auto = $tag->parser->stream->is('auto');
134 | $node = ExtendsNode::create($tag);
135 | if ($auto) {
136 | $node->extends = new AuxiliaryNode(fn() => '$this->global->uiPresenter->findLayoutTemplateFile()');
137 | }
138 | return $node;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationTracy/RoutingPanel.php:
--------------------------------------------------------------------------------
1 | routes = $this->analyse(
41 | $this->router instanceof Routing\RouteList
42 | ? $this->router
43 | : (new Routing\RouteList)->add($this->router),
44 | $this->httpRequest,
45 | );
46 | return Nette\Utils\Helpers::capture(function () {
47 | $matched = $this->matched;
48 | require __DIR__ . '/dist/tab.phtml';
49 | });
50 | }
51 |
52 |
53 | /**
54 | * Renders panel.
55 | */
56 | public function getPanel(): string
57 | {
58 | return Nette\Utils\Helpers::capture(function () {
59 | $matched = $this->matched;
60 | $routes = $this->routes;
61 | $source = $this->matched ? $this->findSource() : null;
62 | $url = $this->httpRequest->getUrl();
63 | $method = $this->httpRequest->getMethod();
64 | require __DIR__ . '/dist/panel.phtml';
65 | });
66 | }
67 |
68 |
69 | private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRequest): array
70 | {
71 | $res = [
72 | 'path' => $router->getPath(),
73 | 'domain' => $router->getDomain(),
74 | 'module' => ($router instanceof Nette\Application\Routers\RouteList ? $router->getModule() : ''),
75 | 'routes' => [],
76 | ];
77 | $httpRequest = $httpRequest
78 | ? (fn() => $this->prepareRequest($httpRequest))->bindTo($router, Routing\RouteList::class)()
79 | : null;
80 | $flags = $router->getFlags();
81 |
82 | foreach ($router->getRouters() as $i => $innerRouter) {
83 | if ($innerRouter instanceof Routing\RouteList) {
84 | $res['routes'][] = $this->analyse($innerRouter, $httpRequest);
85 | continue;
86 | }
87 |
88 | $matched = empty($flags[$i]['oneWay']) ? 'no' : 'oneway';
89 | $params = $e = null;
90 | try {
91 | if (
92 | $httpRequest
93 | && ($params = $innerRouter->match($httpRequest)) !== null
94 | && ($params = (fn() => $this->completeParameters($params))->bindTo($router, Routing\RouteList::class)()) !== null
95 | ) {
96 | $matched = 'may';
97 | if ($this->matched === null) {
98 | $this->matched = $params;
99 | $matched = 'yes';
100 | }
101 | }
102 | } catch (\Throwable $e) {
103 | $matched = 'error';
104 | }
105 |
106 | $res['routes'][] = (object) [
107 | 'matched' => $matched,
108 | 'class' => $innerRouter::class,
109 | 'defaults' => $innerRouter instanceof Routing\Route || $innerRouter instanceof Routing\SimpleRouter ? $innerRouter->getDefaults() : [],
110 | 'mask' => $innerRouter instanceof Routing\Route ? $innerRouter->getMask() : null,
111 | 'params' => $params,
112 | 'error' => $e,
113 | ];
114 | }
115 | return $res;
116 | }
117 |
118 |
119 | private function findSource(): \ReflectionClass|\ReflectionMethod|string|null
120 | {
121 | $params = $this->matched;
122 | $presenter = $params['presenter'] ?? '';
123 | try {
124 | $class = $this->presenterFactory->getPresenterClass($presenter);
125 | } catch (Nette\Application\InvalidPresenterException) {
126 | if ($this->presenterFactory instanceof Nette\Application\PresenterFactory) {
127 | return $this->presenterFactory->formatPresenterClass($presenter);
128 | }
129 | return null;
130 | }
131 |
132 | if (is_a($class, Nette\Application\UI\Presenter::class, allow_string: true)) {
133 | $rc = $class::getReflection();
134 | if (isset($params[Presenter::SignalKey])) {
135 | return $rc->getSignalMethod($params[Presenter::SignalKey]);
136 | } elseif (isset($params[Presenter::ActionKey])
137 | && ($method = $rc->getActionRenderMethod($params[Presenter::ActionKey]))
138 | ) {
139 | return $method;
140 | }
141 | }
142 |
143 | return new \ReflectionClass($class);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/Application/UI/AccessPolicy.php:
--------------------------------------------------------------------------------
1 | presenter ??= $component->getPresenterIfExists() ??
36 | throw new Nette\InvalidStateException('Presenter is required for checking requirements of ' . Reflection::toString($this->element));
37 |
38 | $attrs = $this->getAttributes();
39 | $attrs = self::applyInternalRules($attrs, $component);
40 | foreach ($attrs as $attribute) {
41 | $this->checkAttribute($attribute);
42 | }
43 | }
44 |
45 |
46 | public function canGenerateLink(): bool
47 | {
48 | $attrs = $this->getAttributes();
49 | return !$attrs || !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->forward === true);
50 | }
51 |
52 |
53 | /**
54 | * @return Attributes\Requires[]
55 | */
56 | private function getAttributes(): array
57 | {
58 | return array_map(
59 | fn($ra) => $ra->newInstance(),
60 | $this->element->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF),
61 | );
62 | }
63 |
64 |
65 | private function applyInternalRules(array $attrs, Component $component): array
66 | {
67 | if (
68 | $this->element instanceof \ReflectionMethod
69 | && str_starts_with($this->element->getName(), $component::formatSignalMethod(''))
70 | && !ComponentReflection::parseAnnotation($this->element, 'crossOrigin')
71 | && !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->sameOrigin === false)
72 | ) {
73 | $attrs[] = new Attributes\Requires(sameOrigin: true);
74 | }
75 | return $attrs;
76 | }
77 |
78 |
79 | private function checkAttribute(Attributes\Requires $attribute): void
80 | {
81 | if ($attribute->methods !== null) {
82 | $this->checkHttpMethod($attribute);
83 | }
84 |
85 | if ($attribute->actions !== null) {
86 | $this->checkActions($attribute);
87 | }
88 |
89 | if ($attribute->forward && !$this->presenter->isForwarded()) {
90 | $this->presenter->error('Forwarded request is required by ' . Reflection::toString($this->element));
91 | }
92 |
93 | if ($attribute->sameOrigin && !$this->presenter->getHttpRequest()->isSameSite()) {
94 | $this->presenter->detectedCsrf();
95 | }
96 |
97 | if ($attribute->ajax && !$this->presenter->getHttpRequest()->isAjax()) {
98 | $this->presenter->error('AJAX request is required by ' . Reflection::toString($this->element), Nette\Http\IResponse::S403_Forbidden);
99 | }
100 | }
101 |
102 |
103 | private function checkActions(Attributes\Requires $attribute): void
104 | {
105 | if (
106 | $this->element instanceof \ReflectionMethod
107 | && !$this->element->getDeclaringClass()->isSubclassOf(Presenter::class)
108 | ) {
109 | throw new \LogicException('Requires(actions) used by ' . Reflection::toString($this->element) . ' is allowed only in presenter.');
110 | }
111 |
112 | if (!in_array($this->presenter->getAction(), $attribute->actions, strict: true)) {
113 | $this->presenter->error("Action '{$this->presenter->getAction()}' is not allowed by " . Reflection::toString($this->element));
114 | }
115 | }
116 |
117 |
118 | private function checkHttpMethod(Attributes\Requires $attribute): void
119 | {
120 | if ($this->element instanceof \ReflectionClass) {
121 | $this->presenter->allowedMethods = []; // bypass Presenter::checkHttpMethod()
122 | }
123 |
124 | $allowed = array_map(strtoupper(...), $attribute->methods);
125 | $method = $this->presenter->getHttpRequest()->getMethod();
126 |
127 | if ($allowed !== ['*'] && !in_array($method, $allowed, strict: true)) {
128 | $this->presenter->getHttpResponse()->setHeader('Allow', implode(',', $allowed));
129 | $this->presenter->error(
130 | "Method $method is not allowed by " . Reflection::toString($this->element),
131 | Nette\Http\IResponse::S405_MethodNotAllowed,
132 | );
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/Application/UI/Form.php:
--------------------------------------------------------------------------------
1 | Occurs when form is attached to presenter */
22 | public array $onAnchor = [];
23 |
24 |
25 | /**
26 | * Application form constructor.
27 | */
28 | public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null)
29 | {
30 | parent::__construct();
31 | $parent?->addComponent($this, $name);
32 | }
33 |
34 |
35 | protected function validateParent(Nette\ComponentModel\IContainer $parent): void
36 | {
37 | parent::validateParent($parent);
38 |
39 | $this->monitor(Presenter::class, function (Presenter $presenter): void {
40 | if (!isset($this->getElementPrototype()->id)) {
41 | $this->getElementPrototype()->id = 'frm-' . $this->lookupPath(Presenter::class);
42 | }
43 |
44 | if (!$this->getAction()) {
45 | $this->setAction(new Link($presenter, 'this'));
46 | }
47 |
48 | $controls = $this->getControls();
49 | if (iterator_count($controls) && $this->isSubmitted()) {
50 | foreach ($controls as $control) {
51 | if (!$control->isDisabled()) {
52 | $control->loadHttpData();
53 | }
54 | }
55 | }
56 |
57 | Nette\Utils\Arrays::invoke($this->onAnchor, $this);
58 | });
59 | }
60 |
61 |
62 | /**
63 | * Returns the presenter where this component belongs to.
64 | */
65 | final public function getPresenter(): ?Presenter
66 | {
67 | if (func_num_args()) {
68 | trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getPresenterIfExists()', E_USER_DEPRECATED);
69 | $throw = func_get_arg(0);
70 | }
71 |
72 | return $this->lookup(Presenter::class, throw: $throw ?? true);
73 | }
74 |
75 |
76 | /**
77 | * Returns the presenter where this component belongs to.
78 | */
79 | final public function getPresenterIfExists(): ?Presenter
80 | {
81 | return $this->lookup(Presenter::class, throw: false);
82 | }
83 |
84 |
85 | /** @deprecated */
86 | public function hasPresenter(): bool
87 | {
88 | return (bool) $this->lookup(Presenter::class, throw: false);
89 | }
90 |
91 |
92 | /**
93 | * Tells if the form is anchored.
94 | */
95 | public function isAnchored(): bool
96 | {
97 | return $this->hasPresenter();
98 | }
99 |
100 |
101 | #[\Deprecated('use allowCrossOrigin()')]
102 | public function disableSameSiteProtection(): void
103 | {
104 | $this->allowCrossOrigin();
105 | }
106 |
107 |
108 | /**
109 | * Internal: returns submitted HTTP data or null when form was not submitted.
110 | */
111 | protected function receiveHttpData(): ?array
112 | {
113 | $presenter = $this->getPresenter();
114 | if (!$presenter->isSignalReceiver($this, 'submit')) {
115 | return null;
116 | }
117 |
118 | $request = $presenter->getRequest();
119 | if ($request->isMethod('forward') || $request->isMethod('post') !== $this->isMethod('post')) {
120 | return null;
121 | }
122 |
123 | return $this->isMethod('post')
124 | ? Nette\Utils\Arrays::mergeTree($request->getPost(), $request->getFiles())
125 | : $request->getParameters();
126 | }
127 |
128 |
129 | protected function beforeRender(): void
130 | {
131 | parent::beforeRender();
132 | $key = ($this->isMethod('post') ? '_' : '') . Presenter::SignalKey;
133 | if (!isset($this[$key]) && $this->getAction() !== '') {
134 | $do = $this->lookupPath(Presenter::class) . self::NameSeparator . 'submit';
135 | $this[$key] = (new Nette\Forms\Controls\HiddenField($do))->setOmitted();
136 | }
137 | }
138 |
139 |
140 | /********************* interface SignalReceiver ****************d*g**/
141 |
142 |
143 | /**
144 | * This method is called by presenter.
145 | */
146 | public function signalReceived(string $signal): void
147 | {
148 | if ($signal !== 'submit') {
149 | $class = static::class;
150 | throw new BadSignalException("Missing handler for signal '$signal' in $class.");
151 |
152 | } elseif (!$this->crossOrigin && !$this->getPresenter()->getHttpRequest()->isSameSite()) {
153 | $this->getPresenter()->detectedCsrf();
154 |
155 | } elseif (!$this->getPresenter()->getRequest()->hasFlag(Nette\Application\Request::RESTORED)) {
156 | $this->fireEvents();
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/Application/UI/Control.php:
--------------------------------------------------------------------------------
1 | templateFactory = $templateFactory;
35 | return $this;
36 | }
37 |
38 |
39 | final public function getTemplate(): Template
40 | {
41 | if (!isset($this->template)) {
42 | $this->template = $this->createTemplate();
43 | }
44 |
45 | return $this->template;
46 | }
47 |
48 |
49 | protected function createTemplate(?string $class = null): Template
50 | {
51 | $class ??= $this->formatTemplateClass();
52 | $templateFactory = $this->templateFactory ?? $this->getPresenter()->getTemplateFactory();
53 | return $templateFactory->createTemplate($this, $class);
54 | }
55 |
56 |
57 | public function formatTemplateClass(): ?string
58 | {
59 | return $this->checkTemplateClass(preg_replace('#Control$#', '', static::class) . 'Template');
60 | }
61 |
62 |
63 | /** @internal */
64 | protected function checkTemplateClass(string $class): ?string
65 | {
66 | if (!class_exists($class)) {
67 | return null;
68 | } elseif (!is_a($class, Template::class, allow_string: true)) {
69 | trigger_error(sprintf(
70 | '%s: class %s was found but does not implement the %s, so it will not be used for the template.',
71 | static::class,
72 | $class,
73 | Template::class,
74 | ));
75 | return null;
76 | } else {
77 | return $class;
78 | }
79 | }
80 |
81 |
82 | #[\Deprecated]
83 | public function templatePrepareFilters(Template $template): void
84 | {
85 | }
86 |
87 |
88 | /**
89 | * Saves the message to template, that can be displayed after redirect.
90 | */
91 | public function flashMessage(string|\stdClass|\Stringable $message, string $type = 'info'): \stdClass
92 | {
93 | $id = $this->getParameterId('flash');
94 | $flash = $message instanceof \stdClass ? $message : (object) [
95 | 'message' => $message,
96 | 'type' => $type,
97 | ];
98 | $messages = $this->getPresenter()->getFlashSession()->get($id);
99 | $messages[] = $flash;
100 | $this->getTemplate()->flashes = $messages;
101 | $this->getPresenter()->getFlashSession()->set($id, $messages);
102 | return $flash;
103 | }
104 |
105 |
106 | /********************* rendering ****************d*g**/
107 |
108 |
109 | /**
110 | * Forces control or its snippet to repaint.
111 | */
112 | public function redrawControl(?string $snippet = null, bool $redraw = true): void
113 | {
114 | if ($redraw) {
115 | $this->invalidSnippets[$snippet ?? "\0"] = true;
116 |
117 | } elseif ($snippet === null) {
118 | $this->invalidSnippets = [];
119 |
120 | } else {
121 | $this->invalidSnippets[$snippet] = false;
122 | }
123 | }
124 |
125 |
126 | /**
127 | * Is required to repaint the control or its snippet?
128 | */
129 | public function isControlInvalid(?string $snippet = null): bool
130 | {
131 | if ($snippet !== null) {
132 | return $this->invalidSnippets[$snippet] ?? isset($this->invalidSnippets["\0"]);
133 |
134 | } elseif (count($this->invalidSnippets) > 0) {
135 | return true;
136 | }
137 |
138 | $queue = [$this];
139 | do {
140 | foreach (array_shift($queue)->getComponents() as $component) {
141 | if ($component instanceof Renderable) {
142 | if ($component->isControlInvalid()) {
143 | // $this->invalidSnippets['__child'] = true; // as cache
144 | return true;
145 | }
146 | } elseif ($component instanceof Nette\ComponentModel\IContainer) {
147 | $queue[] = $component;
148 | }
149 | }
150 | } while ($queue);
151 |
152 | return false;
153 | }
154 |
155 |
156 | /**
157 | * Returns snippet HTML ID.
158 | */
159 | public function getSnippetId(string $name): string
160 | {
161 | // HTML 4 ID & NAME: [A-Za-z][A-Za-z0-9:_.-]*
162 | return 'snippet-' . $this->getUniqueId() . '-' . $name;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/Application/MicroPresenter.php:
--------------------------------------------------------------------------------
1 | context;
43 | }
44 |
45 |
46 | public function run(Application\Request $request): Application\Response
47 | {
48 | $this->request = $request;
49 |
50 | if (
51 | $this->httpRequest
52 | && $this->router
53 | && !$this->httpRequest->isAjax()
54 | && ($request->isMethod('get') || $request->isMethod('head'))
55 | ) {
56 | $refUrl = $this->httpRequest->getUrl();
57 | $url = $this->router->constructUrl($request->toArray(), $refUrl);
58 | if ($url !== null && !$refUrl->isEqual($url)) {
59 | return new Responses\RedirectResponse($url, Http\IResponse::S301_MovedPermanently);
60 | }
61 | }
62 |
63 | $params = $request->getParameters();
64 | $callback = $params['callback'] ?? null;
65 | if (!is_object($callback) || !is_callable($callback)) {
66 | throw new Application\BadRequestException('Parameter callback is not a valid closure.');
67 | }
68 |
69 | $reflection = Nette\Utils\Callback::toReflection($callback);
70 |
71 | if ($this->context) {
72 | foreach ($reflection->getParameters() as $param) {
73 | $type = $param->getType();
74 | if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
75 | $params[$param->getName()] = $this->context->getByType($type->getName(), throw: false);
76 | }
77 | }
78 | }
79 |
80 | $params['presenter'] = $this;
81 | try {
82 | $params = Application\UI\ParameterConverter::toArguments($reflection, $params);
83 | } catch (Nette\InvalidArgumentException $e) {
84 | $this->error($e->getMessage());
85 | }
86 |
87 | $response = $callback(...array_values($params));
88 |
89 | if (is_string($response)) {
90 | $response = [$response, []];
91 | }
92 |
93 | if (is_array($response)) {
94 | [$templateSource, $templateParams] = $response;
95 | $response = $this->createTemplate()->setParameters($templateParams);
96 | if (!$templateSource instanceof \SplFileInfo) {
97 | $response->getLatte()->setLoader(new Latte\Loaders\StringLoader);
98 | }
99 |
100 | $response->setFile((string) $templateSource);
101 | }
102 |
103 | if ($response instanceof Application\UI\Template) {
104 | return new Responses\TextResponse($response);
105 | } else {
106 | return $response ?: new Responses\VoidResponse;
107 | }
108 | }
109 |
110 |
111 | /**
112 | * Template factory.
113 | */
114 | public function createTemplate(?string $class = null, ?callable $latteFactory = null): Application\UI\Template
115 | {
116 | $latte = $latteFactory
117 | ? $latteFactory()
118 | : $this->getContext()->getByType(Nette\Bridges\ApplicationLatte\LatteFactory::class)->create();
119 | $template = $class
120 | ? new $class
121 | : new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte);
122 |
123 | $template->setParameters($this->request->getParameters());
124 | $template->presenter = $this;
125 | $template->context = $this->context;
126 | if ($this->httpRequest) {
127 | $url = $this->httpRequest->getUrl();
128 | $template->baseUrl = rtrim($url->getBaseUrl(), '/');
129 | $template->basePath = rtrim($url->getBasePath(), '/');
130 | }
131 |
132 | return $template;
133 | }
134 |
135 |
136 | /**
137 | * Redirects to another URL.
138 | */
139 | public function redirectUrl(string $url, int $httpCode = Http\IResponse::S302_Found): Responses\RedirectResponse
140 | {
141 | return new Responses\RedirectResponse($url, $httpCode);
142 | }
143 |
144 |
145 | /**
146 | * Throws HTTP error.
147 | * @throws Nette\Application\BadRequestException
148 | */
149 | public function error(string $message = '', int $httpCode = Http\IResponse::S404_NotFound): void
150 | {
151 | throw new Application\BadRequestException($message, $httpCode);
152 | }
153 |
154 |
155 | public function getRequest(): ?Nette\Application\Request
156 | {
157 | return $this->request;
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationDI/LatteExtension.php:
--------------------------------------------------------------------------------
1 | Expect::anyOf(true, false, 'all'),
37 | 'extensions' => Expect::arrayOf('string|Nette\DI\Definitions\Statement'),
38 | 'templateClass' => Expect::string(),
39 | 'strictTypes' => Expect::bool(false),
40 | 'strictParsing' => Expect::bool(false),
41 | 'phpLinter' => Expect::string(),
42 | 'locale' => Expect::string(),
43 | ]);
44 | }
45 |
46 |
47 | public function loadConfiguration(): void
48 | {
49 | if (!class_exists(Latte\Engine::class)) {
50 | return;
51 | }
52 |
53 | $config = $this->config;
54 | $builder = $this->getContainerBuilder();
55 |
56 | $builder->addFactoryDefinition($this->prefix('latteFactory'))
57 | ->setImplement(ApplicationLatte\LatteFactory::class)
58 | ->getResultDefinition()
59 | ->setFactory(Latte\Engine::class)
60 | ->addSetup('setTempDirectory', [$this->tempDir])
61 | ->addSetup('setAutoRefresh', [$this->debugMode])
62 | ->addSetup('setStrictTypes', [$config->strictTypes])
63 | ->addSetup('setStrictParsing', [$config->strictParsing])
64 | ->addSetup('enablePhpLinter', [$config->phpLinter])
65 | ->addSetup('setLocale', [$config->locale])
66 | ->addSetup('addExtension', [new Statement(ApplicationLatte\UIExtension::class, [$builder::literal('$control')])]);
67 |
68 | if ($builder->getByType(Nette\Caching\Storage::class)) {
69 | $this->addExtension(new Statement(Nette\Bridges\CacheLatte\CacheExtension::class));
70 | }
71 | if (class_exists(Nette\Bridges\FormsLatte\FormsExtension::class)) {
72 | $this->addExtension(new Statement(Nette\Bridges\FormsLatte\FormsExtension::class));
73 | }
74 |
75 | foreach ($config->extensions as $extension) {
76 | if ($extension === Latte\Essential\TranslatorExtension::class) {
77 | $extension = new Statement($extension, [new Nette\DI\Definitions\Reference(Nette\Localization\Translator::class)]);
78 | }
79 | $this->addExtension($extension);
80 | }
81 |
82 | $builder->addDefinition($this->prefix('templateFactory'))
83 | ->setFactory(ApplicationLatte\TemplateFactory::class)
84 | ->setArguments(['templateClass' => $config->templateClass]);
85 |
86 | if ($this->name === 'latte') {
87 | $builder->addAlias('nette.latteFactory', $this->prefix('latteFactory'));
88 | $builder->addAlias('nette.templateFactory', $this->prefix('templateFactory'));
89 | }
90 | }
91 |
92 |
93 | public function beforeCompile(): void
94 | {
95 | $builder = $this->getContainerBuilder();
96 |
97 | if (
98 | $this->debugMode
99 | && ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class))
100 | && class_exists(Latte\Bridges\Tracy\LattePanel::class)
101 | ) {
102 | $factory = $builder->getDefinition($this->prefix('templateFactory'));
103 | $factory->addSetup([self::class, 'initLattePanel'], [$factory, 'all' => $this->config->debugger === 'all']);
104 | }
105 | }
106 |
107 |
108 | public static function initLattePanel(
109 | Nette\Application\UI\TemplateFactory $factory,
110 | Tracy\Bar $bar,
111 | bool $all = false,
112 | ): void
113 | {
114 | if (!$factory instanceof ApplicationLatte\TemplateFactory) {
115 | return;
116 | }
117 |
118 | $factory->onCreate[] = function (ApplicationLatte\Template $template) use ($bar, $all) {
119 | $control = $template->getLatte()->getProviders()['uiControl'] ?? null;
120 | if ($all || $control instanceof Nette\Application\UI\Presenter) {
121 | $name = $all && $control ? (new \ReflectionObject($control))->getShortName() : '';
122 | $template->getLatte()->addExtension(new Latte\Bridges\Tracy\TracyExtension($name));
123 | }
124 | };
125 | }
126 |
127 |
128 | public function addExtension(Statement|string $extension): void
129 | {
130 | $extension = is_string($extension)
131 | ? new Statement($extension)
132 | : $extension;
133 |
134 | $builder = $this->getContainerBuilder();
135 | $builder->getDefinition($this->prefix('latteFactory'))
136 | ->getResultDefinition()
137 | ->addSetup('addExtension', [$extension]);
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/Nodes/SnippetNode.php:
--------------------------------------------------------------------------------
1 | */
40 | public static function create(Tag $tag, TemplateParser $parser): \Generator
41 | {
42 | $tag->outputMode = $tag::OutputKeepIndentation;
43 |
44 | $node = $tag->node = new static;
45 | $node->htmlElement = $tag->isNAttribute() ? $tag->htmlElement : null;
46 |
47 | if ($tag->parser->isEnd()) {
48 | $name = null;
49 | $node->block = new Block(new Scalar\StringNode(''), Template::LayerSnippet, $tag);
50 | } else {
51 | $name = $tag->parser->parseUnquotedStringOrExpression();
52 | if (
53 | $name instanceof Expression\ClassConstantFetchNode
54 | && $name->class instanceof Php\NameNode
55 | && $name->name instanceof Php\IdentifierNode
56 | ) {
57 | $name = new Scalar\StringNode(constant($name->class . '::' . $name->name), $name->position);
58 | }
59 | $node->block = new Block($name, Template::LayerSnippet, $tag);
60 | if (!$node->block->isDynamic()) {
61 | $parser->checkBlockIsUnique($node->block);
62 | }
63 | }
64 |
65 | if ($tag->isNAttribute()) {
66 | if ($tag->prefix !== $tag::PrefixNone) {
67 | throw new CompileException("Use n:snippet instead of {$tag->getNotation()}", $tag->position);
68 |
69 | } elseif ($tag->htmlElement->getAttribute(self::$snippetAttribute)) {
70 | throw new CompileException('Cannot combine HTML attribute ' . self::$snippetAttribute . ' with n:snippet.', $tag->position);
71 |
72 | } elseif (isset($tag->htmlElement->nAttributes['ifcontent'])) {
73 | throw new CompileException('Cannot combine n:ifcontent with n:snippet.', $tag->position);
74 |
75 | } elseif (isset($tag->htmlElement->nAttributes['foreach'])) {
76 | throw new CompileException('Combination of n:snippet with n:foreach is invalid, use n:inner-foreach.', $tag->position);
77 | }
78 |
79 | $tag->replaceNAttribute(new AuxiliaryNode(
80 | fn(PrintContext $context) => "echo ' " . $node->printAttribute($context) . "';",
81 | ));
82 | }
83 |
84 | [$node->content, $endTag] = yield;
85 | if ($endTag && $name instanceof Scalar\StringNode) {
86 | $endTag->parser->stream->tryConsume($name->value);
87 | }
88 |
89 | return $node;
90 | }
91 |
92 |
93 | public function print(PrintContext $context): string
94 | {
95 | if (!$this->block->isDynamic()) {
96 | $context->addBlock($this->block);
97 | }
98 |
99 | if ($this->htmlElement) {
100 | try {
101 | $inner = $this->htmlElement->content;
102 | $this->htmlElement->content = new AuxiliaryNode(fn() => $this->printContent($context, $inner));
103 | return $this->content->print($context);
104 | } finally {
105 | $this->htmlElement->content = $inner;
106 | }
107 | } else {
108 | return <<printAttribute($context)}>';
110 | {$this->printContent($context, $this->content)}
111 | echo '';
112 | XX;
113 | }
114 | }
115 |
116 |
117 | private function printContent(PrintContext $context, AreaNode $inner): string
118 | {
119 | $dynamic = $this->block->isDynamic();
120 | $res = $context->format(
121 | <<<'XX'
122 | $this->global->snippetDriver->enter(%node, %dump) %line;
123 | try {
124 | %node
125 | } finally {
126 | $this->global->snippetDriver->leave();
127 | }
128 |
129 | XX,
130 | $dynamic ? new AuxiliaryNode(fn() => '$ʟ_nm') : $this->block->name,
131 | $dynamic ? SnippetRuntime::TypeDynamic : SnippetRuntime::TypeStatic,
132 | $this->position,
133 | $inner,
134 | );
135 |
136 | if ($dynamic) {
137 | return $res;
138 | }
139 |
140 | $this->block->content = $res;
141 | return $context->format(
142 | '$this->renderBlock(%node, [], null, %dump) %line;',
143 | $this->block->name,
144 | Template::LayerSnippet,
145 | $this->position,
146 | );
147 | }
148 |
149 |
150 | private function printAttribute(PrintContext $context): string
151 | {
152 | return $context->format(
153 | <<<'XX'
154 | %raw="', htmlspecialchars($this->global->snippetDriver->getHtmlId(%node)), '"
155 | XX,
156 | self::$snippetAttribute,
157 | $this->block->isDynamic()
158 | ? new Expression\AssignNode(new Expression\VariableNode('ʟ_nm'), $this->block->name)
159 | : $this->block->name,
160 | );
161 | }
162 |
163 |
164 | public function &getIterator(): \Generator
165 | {
166 | yield $this->block->name;
167 | yield $this->content;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/Application/UI/ParameterConverter.php:
--------------------------------------------------------------------------------
1 | getParameters() as $i => $param) {
29 | $name = $param->getName();
30 | $type = self::getType($param);
31 | if (isset($args[$name])) {
32 | $res[$i] = $args[$name];
33 | if (!self::convertType($res[$i], $type)) {
34 | throw new Nette\InvalidArgumentException(sprintf(
35 | 'Argument $%s passed to %s must be %s, %s given.',
36 | $name,
37 | Reflection::toString($method),
38 | $type,
39 | get_debug_type($args[$name]),
40 | ));
41 | }
42 | } elseif ($param->isDefaultValueAvailable()) {
43 | $res[$i] = $param->getDefaultValue();
44 | } elseif ($type === 'scalar' || $param->allowsNull()) {
45 | $res[$i] = null;
46 | } elseif ($type === 'array' || $type === 'iterable') {
47 | $res[$i] = [];
48 | } else {
49 | throw new Nette\InvalidArgumentException(sprintf(
50 | 'Missing parameter $%s required by %s',
51 | $name,
52 | Reflection::toString($method),
53 | ));
54 | }
55 | }
56 |
57 | return $res;
58 | }
59 |
60 |
61 | /**
62 | * Converts list of arguments to named parameters & check types.
63 | * @param \ReflectionParameter[] $missing arguments
64 | * @throws InvalidLinkException
65 | * @internal
66 | */
67 | public static function toParameters(
68 | \ReflectionMethod $method,
69 | array &$args,
70 | array $supplemental = [],
71 | ?array &$missing = null,
72 | ): void
73 | {
74 | $i = 0;
75 | foreach ($method->getParameters() as $param) {
76 | $type = self::getType($param);
77 | $name = $param->getName();
78 |
79 | if (array_key_exists($i, $args)) {
80 | $args[$name] = $args[$i];
81 | unset($args[$i]);
82 | $i++;
83 |
84 | } elseif (array_key_exists($name, $args)) {
85 | // continue with process
86 |
87 | } elseif (array_key_exists($name, $supplemental)) {
88 | $args[$name] = $supplemental[$name];
89 | }
90 |
91 | if (!isset($args[$name])) {
92 | if (
93 | !$param->isDefaultValueAvailable()
94 | && !$param->allowsNull()
95 | && $type !== 'scalar'
96 | && $type !== 'array'
97 | && $type !== 'iterable'
98 | ) {
99 | $missing[] = $param;
100 | unset($args[$name]);
101 | }
102 |
103 | continue;
104 | }
105 |
106 | if (!self::convertType($args[$name], $type)) {
107 | throw new InvalidLinkException(sprintf(
108 | 'Argument $%s passed to %s must be %s, %s given.',
109 | $name,
110 | Reflection::toString($method),
111 | $type,
112 | get_debug_type($args[$name]),
113 | ));
114 | }
115 |
116 | $def = $param->isDefaultValueAvailable()
117 | ? $param->getDefaultValue()
118 | : null;
119 | if ($args[$name] === $def || ($def === null && $args[$name] === '')) {
120 | $args[$name] = null; // value transmit is unnecessary
121 | }
122 | }
123 |
124 | if (array_key_exists($i, $args)) {
125 | throw new InvalidLinkException('Passed more parameters than method ' . Reflection::toString($method) . ' expects.');
126 | }
127 | }
128 |
129 |
130 | /**
131 | * Lossless type conversion.
132 | */
133 | public static function convertType(mixed &$val, string $types): bool
134 | {
135 | $scalars = ['string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'true' => 1, 'false' => 1];
136 | $testable = ['iterable' => 1, 'object' => 1, 'array' => 1, 'null' => 1];
137 |
138 | foreach (explode('|', ltrim($types, '?')) as $type) {
139 | if (match (true) {
140 | isset($scalars[$type]) => self::castScalar($val, $type),
141 | isset($testable[$type]) => "is_$type"($val),
142 | $type === 'scalar' => !is_array($val), // special type due to historical reasons
143 | $type === 'mixed' => true,
144 | $type === 'callable' => false, // intentionally disabled for security reasons
145 | default => $val instanceof $type,
146 | }) {
147 | return true;
148 | }
149 | }
150 |
151 | return false;
152 | }
153 |
154 |
155 | /**
156 | * Lossless type casting.
157 | */
158 | private static function castScalar(mixed &$val, string $type): bool
159 | {
160 | if (!is_scalar($val)) {
161 | return false;
162 | }
163 |
164 | $tmp = ($val === false ? '0' : (string) $val);
165 | if ($type === 'float') {
166 | $tmp = preg_replace('#\.0*$#D', '', $tmp);
167 | }
168 |
169 | $orig = $tmp;
170 | $spec = ['true' => true, 'false' => false];
171 | isset($spec[$type]) ? $tmp = $spec[$type] : settype($tmp, $type);
172 | if ($orig !== ($tmp === false ? '0' : (string) $tmp)) {
173 | return false; // data-loss occurs
174 | }
175 |
176 | $val = $tmp;
177 | return true;
178 | }
179 |
180 |
181 | public static function getType(\ReflectionParameter|\ReflectionProperty $item): string
182 | {
183 | if ($type = $item->getType()) {
184 | return (string) $type;
185 | }
186 | $default = $item instanceof \ReflectionProperty || $item->isDefaultValueAvailable()
187 | ? $item->getDefaultValue()
188 | : null;
189 | return $default === null ? 'scalar' : get_debug_type($default);
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/Application/UI/ComponentReflection.php:
--------------------------------------------------------------------------------
1 | $name
21 | * @property-read string $fileName
22 | * @internal
23 | */
24 | final class ComponentReflection extends \ReflectionClass
25 | {
26 | private static array $ppCache = [];
27 | private static array $pcCache = [];
28 | private static array $armCache = [];
29 |
30 |
31 | /**
32 | * Returns array of class properties that are public and have attribute #[Persistent] or #[Parameter].
33 | * @return array
34 | */
35 | public function getParameters(): array
36 | {
37 | $params = &self::$ppCache[$this->getName()];
38 | if ($params !== null) {
39 | return $params;
40 | }
41 |
42 | $params = [];
43 | $isPresenter = $this->isSubclassOf(Presenter::class);
44 | foreach ($this->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
45 | if ($prop->isStatic()) {
46 | continue;
47 | } elseif ($prop->getAttributes(Attributes\Persistent::class)) {
48 | $params[$prop->getName()] = [
49 | 'def' => $prop->hasDefaultValue() ? $prop->getDefaultValue() : null,
50 | 'type' => ParameterConverter::getType($prop),
51 | 'since' => $isPresenter ? Reflection::getPropertyDeclaringClass($prop)->getName() : null,
52 | ];
53 | } elseif ($prop->getAttributes(Attributes\Parameter::class)) {
54 | $params[$prop->getName()] = [
55 | 'def' => $prop->hasDefaultValue() ? $prop->getDefaultValue() : null,
56 | 'type' => (string) ($prop->getType() ?? 'mixed'),
57 | ];
58 | }
59 | }
60 |
61 | if ($this->getParentClass()->isSubclassOf(Component::class)) {
62 | $parent = new self($this->getParentClass()->getName());
63 | foreach ($parent->getParameters() as $name => $meta) {
64 | if (!isset($params[$name])) {
65 | $params[$name] = $meta;
66 | } elseif (array_key_exists('since', $params[$name])) {
67 | $params[$name]['since'] = $meta['since'];
68 | }
69 | }
70 | }
71 |
72 | return $params;
73 | }
74 |
75 |
76 | /**
77 | * Returns array of persistent properties. They are public and have attribute #[Persistent].
78 | * @return array
79 | */
80 | public function getPersistentParams(): array
81 | {
82 | return array_filter($this->getParameters(), fn($param) => array_key_exists('since', $param));
83 | }
84 |
85 |
86 | /**
87 | * Returns array of persistent components. They are tagged with class-level attribute
88 | * #[Persistent] or returned by Presenter::getPersistentComponents().
89 | * @return array
90 | */
91 | public function getPersistentComponents(): array
92 | {
93 | $class = $this->name;
94 | $components = &self::$pcCache[$class];
95 | if ($components !== null) {
96 | return $components;
97 | }
98 |
99 | $attrs = $this->getAttributes(Attributes\Persistent::class);
100 | $names = $attrs ? $attrs[0]->getArguments() : [];
101 | $names = array_merge($names, $class::getPersistentComponents());
102 | $components = array_fill_keys($names, ['since' => $class]);
103 |
104 | if ($this->isSubclassOf(Presenter::class)) {
105 | $parent = new self($this->getParentClass()->getName());
106 | $components = $parent->getPersistentComponents() + $components;
107 | }
108 |
109 | return $components;
110 | }
111 |
112 |
113 | public function getTemplateVariables(Control $control): array
114 | {
115 | $res = [];
116 | foreach ($this->getProperties() as $prop) {
117 | if ($prop->getAttributes(Attributes\TemplateVariable::class)) {
118 | $name = $prop->getName();
119 | if (!$prop->isPublic()) {
120 | throw new \LogicException("Property {$this->getName()}::\$$name must be public to be used as TemplateVariable.");
121 | } elseif ($prop->isInitialized($control) || (PHP_VERSION_ID >= 80400 && $prop->hasHook(\PropertyHookType::Get))) {
122 | $res[] = $name;
123 | }
124 | }
125 | }
126 | return $res;
127 | }
128 |
129 |
130 | /**
131 | * Is a method callable? It means class is instantiable and method has
132 | * public visibility, is non-static and non-abstract.
133 | */
134 | public function hasCallableMethod(string $method): bool
135 | {
136 | return $this->isInstantiable()
137 | && $this->hasMethod($method)
138 | && ($rm = $this->getMethod($method))
139 | && $rm->isPublic() && !$rm->isAbstract() && !$rm->isStatic();
140 | }
141 |
142 |
143 | /** Returns action*() or render*() method if available */
144 | public function getActionRenderMethod(string $action): ?\ReflectionMethod
145 | {
146 | $class = $this->name;
147 | return self::$armCache[$class][$action] ??=
148 | $this->hasCallableMethod($name = $class::formatActionMethod($action))
149 | || $this->hasCallableMethod($name = $class::formatRenderMethod($action))
150 | ? parent::getMethod($name)
151 | : null;
152 | }
153 |
154 |
155 | /** Returns handle*() method if available */
156 | public function getSignalMethod(string $signal): ?\ReflectionMethod
157 | {
158 | $class = $this->name;
159 | return $this->hasCallableMethod($name = $class::formatSignalMethod($signal))
160 | ? parent::getMethod($name)
161 | : null;
162 | }
163 |
164 |
165 | #[\Deprecated]
166 | public static function combineArgs(\ReflectionFunctionAbstract $method, array $args): array
167 | {
168 | return ParameterConverter::toArguments($method, $args);
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/Application/Routers/Route.php:
--------------------------------------------------------------------------------
1 | [
28 | self::Pattern => '[a-z][a-z0-9.-]*',
29 | self::FilterIn => [self::class, 'path2presenter'],
30 | self::FilterOut => [self::class, 'presenter2path'],
31 | ],
32 | 'presenter' => [
33 | self::Pattern => '[a-z][a-z0-9.-]*',
34 | self::FilterIn => [self::class, 'path2presenter'],
35 | self::FilterOut => [self::class, 'presenter2path'],
36 | ],
37 | 'action' => [
38 | self::Pattern => '[a-z][a-z0-9-]*',
39 | self::FilterIn => [self::class, 'path2action'],
40 | self::FilterOut => [self::class, 'action2path'],
41 | ],
42 | ];
43 |
44 |
45 | /**
46 | * @param string $mask e.g. '//'
47 | * @param array|string|\Closure $metadata default values or metadata or callback for NetteModule\MicroPresenter
48 | */
49 | public function __construct(string $mask, array|string|\Closure $metadata = [])
50 | {
51 | if (is_string($metadata)) {
52 | [$presenter, $action] = Nette\Application\Helpers::splitName($metadata);
53 | if (!$presenter) {
54 | throw new Nette\InvalidArgumentException("Second argument must be array or string in format Presenter:action, '$metadata' given.");
55 | }
56 |
57 | $metadata = [self::PresenterKey => $presenter];
58 | if ($action !== '') {
59 | $metadata['action'] = $action;
60 | }
61 | } elseif ($metadata instanceof \Closure) {
62 | $metadata = [
63 | self::PresenterKey => 'Nette:Micro',
64 | 'callback' => $metadata,
65 | ];
66 | }
67 |
68 | $this->defaultMeta += self::UIMeta;
69 | parent::__construct($mask, $metadata);
70 | }
71 |
72 |
73 | /**
74 | * Maps HTTP request to an array.
75 | */
76 | public function match(Nette\Http\IRequest $httpRequest): ?array
77 | {
78 | $params = parent::match($httpRequest);
79 |
80 | if ($params === null) {
81 | return null;
82 | } elseif (!isset($params[self::PresenterKey])) {
83 | throw new Nette\InvalidStateException('Missing presenter in route definition.');
84 | } elseif (!is_string($params[self::PresenterKey])) {
85 | return null;
86 | }
87 |
88 | $presenter = $params[self::PresenterKey] ?? null;
89 | if (isset($this->getMetadata()[self::ModuleKey], $params[self::ModuleKey]) && is_string($presenter)) {
90 | $params[self::PresenterKey] = $params[self::ModuleKey] . ':' . $params[self::PresenterKey];
91 | }
92 |
93 | unset($params[self::ModuleKey]);
94 |
95 | return $params;
96 | }
97 |
98 |
99 | /**
100 | * Constructs absolute URL from array.
101 | */
102 | public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string
103 | {
104 | $metadata = $this->getMetadata();
105 | if (isset($metadata[self::ModuleKey])) { // try split into module and [submodule:]presenter parts
106 | $presenter = $params[self::PresenterKey];
107 | $module = $metadata[self::ModuleKey];
108 | $a = isset($module['fixity'], $module[self::Value])
109 | && strncmp($presenter, $module[self::Value] . ':', strlen($module[self::Value]) + 1) === 0
110 | ? strlen($module[self::Value])
111 | : strrpos($presenter, ':');
112 | if ($a === false) {
113 | $params[self::ModuleKey] = isset($module[self::Value]) ? '' : null;
114 | } else {
115 | $params[self::ModuleKey] = substr($presenter, 0, $a);
116 | $params[self::PresenterKey] = substr($presenter, $a + 1);
117 | }
118 | }
119 |
120 | return parent::constructUrl($params, $refUrl);
121 | }
122 |
123 |
124 | /** @internal */
125 | public function getConstantParameters(): array
126 | {
127 | $res = parent::getConstantParameters();
128 | if (isset($res[self::ModuleKey], $res[self::PresenterKey])) {
129 | $res[self::PresenterKey] = $res[self::ModuleKey] . ':' . $res[self::PresenterKey];
130 | } elseif (isset($this->getMetadata()[self::ModuleKey])) {
131 | unset($res[self::PresenterKey]);
132 | }
133 |
134 | unset($res[self::ModuleKey]);
135 | return $res;
136 | }
137 |
138 |
139 | /********************* Inflectors ****************d*g**/
140 |
141 |
142 | /**
143 | * camelCaseAction name -> dash-separated.
144 | */
145 | public static function action2path(string $s): string
146 | {
147 | $s = preg_replace('#(.)(?=[A-Z])#', '$1-', $s);
148 | $s = strtolower($s);
149 | $s = rawurlencode($s);
150 | return $s;
151 | }
152 |
153 |
154 | /**
155 | * dash-separated -> camelCaseAction name.
156 | */
157 | public static function path2action(string $s): string
158 | {
159 | $s = preg_replace('#-(?=[a-z])#', ' ', $s);
160 | $s = lcfirst(ucwords($s));
161 | $s = str_replace(' ', '', $s);
162 | return $s;
163 | }
164 |
165 |
166 | /**
167 | * PascalCase:Presenter name -> dash-and-dot-separated.
168 | */
169 | public static function presenter2path(string $s): string
170 | {
171 | $s = strtr($s, ':', '.');
172 | $s = preg_replace('#([^.])(?=[A-Z])#', '$1-', $s);
173 | $s = strtolower($s);
174 | $s = rawurlencode($s);
175 | return $s;
176 | }
177 |
178 |
179 | /**
180 | * dash-and-dot-separated -> PascalCase:Presenter name.
181 | */
182 | public static function path2presenter(string $s): string
183 | {
184 | $s = preg_replace('#([.-])(?=[a-z])#', '$1 ', $s);
185 | $s = ucwords($s);
186 | $s = str_replace('. ', ':', $s);
187 | $s = str_replace('- ', '', $s);
188 | return $s;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationTracy/panel.latte:
--------------------------------------------------------------------------------
1 | {use Nette\Application\UI\Presenter}
2 | {use Tracy\Dumper}
3 |
4 |
81 |
82 |
83 | {if $matched === null}
84 | no route
85 | {elseif isset($matched[Presenter::PresenterKey])}
86 | {$matched[Presenter::PresenterKey]}:{$matched[Presenter::ActionKey] ?? Presenter::DefaultAction}
87 | {if isset($matched[Presenter::SignalKey])}{$matched[Presenter::SignalKey]}!{/if}
88 | {/if}
89 |
90 |
91 |
92 |
104 |
105 |
106 | {if empty($routes)}
107 |
No routes defined.
108 | {else}
109 |
110 |
111 |
112 |
Mask / Class
113 |
Defaults
114 |
Matched as
115 |
116 |
117 | {define routeList $list, $path = ''}
118 |
119 | {if $list[domain] || $list[module]}
120 |
124 | {/if}
125 | {do $path .= $list[path]}
126 | {foreach $list[routes] as $router}
127 | {if is_array($router)}
128 | {include routeList $router, $path}
129 | {else}
130 | {include route $router, $path}
131 | {/if}
132 | {/foreach}
133 |
134 | {/define}
135 |
136 | {define route $route, $path}
137 |
138 |
139 | {=[yes => '✓', may => '≈', no => '', oneway => '⛔', error => '❌'][$route->matched]}
140 |
141 |
142 |
143 |
144 | {if $path !== ''}{$path}{/if}
145 | {isset($route->mask) ? str_replace(['/', '-'], ['/', '-'], htmlspecialchars($route->mask)) : str_replace('\\', '\\', htmlspecialchars($route->class))|noescape}
146 |
147 |
148 |
149 |
150 |
151 | {foreach $route->defaults as $key => $value}
152 | {$key} = {if is_string($value)}{$value}
{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if}
153 | {/foreach}
154 |
155 |
156 |
157 |
158 | {if $route->params}
159 |
160 | {do $params = $route->params}
161 | {if isset($params[Presenter::PresenterKey])}
162 | {$params[presenter]}:{$params[Presenter::ActionKey] ?? Presenter::DefaultAction}
163 |
164 | {do unset($params[Presenter::PresenterKey], $params[Presenter::ActionKey])}
165 | {/if}
166 | {foreach $params as $key => $value}
167 | {$key} = {if is_string($value)}{$value}
{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if}
168 | {/foreach}
169 |
170 | {elseif $route->error}
171 | {$route->error->getMessage()}
172 | {/if}
173 |
174 |
175 | {/define}
176 |
177 | {include routeList $routes}
178 |
179 | {/if}
180 |
181 |
182 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationLatte/TemplateGenerator.php:
--------------------------------------------------------------------------------
1 | */
30 | private array $properties = [];
31 | private bool $trackProperties = true;
32 |
33 |
34 | public function __construct(
35 | Latte\Engine $latte,
36 | ?string $className = null,
37 | ?UI\Control $control = null,
38 | ) {
39 | parent::__construct($latte);
40 |
41 | $this->className = $className && $className !== DefaultTemplate::class
42 | ? $className
43 | : preg_replace('#Control|Presenter$#', '', $control::class) . 'Template';
44 |
45 | if (!class_exists($this->className)) {
46 | $this->createTemplateClass($control);
47 | $this->updateControlPhpDoc($control);
48 | }
49 | $this->loadTemplateClass();
50 | }
51 |
52 |
53 | public function __set($name, $value): void
54 | {
55 | if ($this->trackProperties) {
56 | ($this->findPropertyOwner($name) ?? $this)->ensureProperty($name, $value);
57 | }
58 | $this->$name = $value;
59 | }
60 |
61 |
62 | public function addDefaultVariable(string $name, mixed $value): void
63 | {
64 | $owner = $this->findPropertyOwner($name) ?? $this;
65 | if (!isset($owner->properties[$name])) {
66 | if (is_object($value)) {
67 | if (PHP_VERSION_ID >= 80400) {
68 | $rc = new \ReflectionClass($value);
69 | $rc->initializeLazyObject($value);
70 | $value = ($rc)->newLazyProxy(fn() => $this->ensureProperty($name, $value));
71 | }
72 |
73 | } else {
74 | $value = new class (fn() => $this->ensureProperty($name, $value)) implements \IteratorAggregate {
75 | public function __construct(
76 | private \Closure $cb,
77 | ) {
78 | }
79 |
80 |
81 | public function __toString(): string
82 | {
83 | return ($this->cb)(); // basePath & baseUrl
84 | }
85 |
86 |
87 | public function getIterator(): \Traversable
88 | {
89 | yield from ($this->cb)(); // flashes
90 | }
91 | };
92 | }
93 | }
94 |
95 | $this->trackProperties = false;
96 | $this->$name = $value;
97 | $this->trackProperties = true;
98 | }
99 |
100 |
101 | private function ensureProperty(string $name, mixed $value): mixed
102 | {
103 | $declaredType = $this->properties[$name] ?? null;
104 | $actualType = Type::fromValue($value);
105 | if (!$declaredType) {
106 | $this->properties[$name] = $actualType;
107 | $this->updateTemplateClass($name);
108 | } elseif (!$declaredType->allows($actualType)) {
109 | $this->properties[$name] = $declaredType->with($actualType);
110 | $this->updateTemplateClass($name);
111 | }
112 | return $value;
113 | }
114 |
115 |
116 | private function findPropertyOwner(string $name): ?self
117 | {
118 | return match (true) {
119 | isset($this->properties[$name]) => $this,
120 | $this->parent !== null => $this->parent->findPropertyOwner($name),
121 | default => null,
122 | };
123 | }
124 |
125 |
126 | /********************* generator ****************d*g**/
127 |
128 |
129 | private function createTemplateClass(UI\Control $control): void
130 | {
131 | [$namespace, $shortName] = Helpers::splitClassName($this->className);
132 | $namespaceCode = $namespace ? PHP_EOL . "namespace $namespace;" . PHP_EOL : '';
133 | $fileName = dirname((new \ReflectionClass($control))->getFileName()) . '/' . $shortName . '.php';
134 | file_put_contents($fileName, <<className);
152 | foreach ($rc->getProperties() as $prop) {
153 | if ($prop->getDeclaringClass() == $rc) { // intentionally ==
154 | $this->properties[$prop->getName()] = Type::fromReflection($prop);
155 | }
156 | }
157 |
158 | $parent = $rc->getParentClass()->getName();
159 | if ($parent !== Template::class) {
160 | $this->parent = new self($this->getLatte(), $parent);
161 | }
162 | }
163 |
164 |
165 | private function updateTemplateClass(string $name): void
166 | {
167 | $file = (new \ReflectionClass($this->className))->getFileName();
168 | if (!is_file($file)) {
169 | throw new \RuntimeException("Cannot update class file for {$this->className}, file not found.");
170 | }
171 |
172 | $src = file_get_contents($file);
173 | $typeDecl = preg_replace_callback(
174 | '/([\w\\\]+)/',
175 | fn($m) => Validators::isBuiltinType($m[1]) ? $m[1] : '\\' . $m[1],
176 | (string) $this->properties[$name],
177 | );
178 | $decl = "\tpublic $typeDecl \$$name";
179 |
180 | $src = preg_replace(
181 | '/^\s*public\s+[^$;]*\s*\$' . $name . '\b/m',
182 | $decl,
183 | $src,
184 | count: $count,
185 | );
186 | if (!$count) {
187 | if ($pos = strrpos($src, '}')) {
188 | $src = substr_replace($src, $decl . ';' . PHP_EOL, $pos, 0);
189 | } else {
190 | throw new \RuntimeException("Cannot update class file for {$this->className}, invalid syntax.");
191 | }
192 | }
193 | file_put_contents($file, $src);
194 | }
195 |
196 |
197 | private function updateControlPhpDoc(UI\Control $control): void
198 | {
199 | $rc = new \ReflectionClass($control);
200 | $doc = $rc->getDocComment();
201 | $content = file_get_contents($rc->getFileName());
202 | $nl = PHP_EOL;
203 | $decl = '* @property-read ' . Helpers::splitClassName($this->className)[1] . ' $template';
204 |
205 | if (!$doc) {
206 | $content = preg_replace(
207 | '/^((final\s+)class\s+' . $rc->getShortName() . ')/m',
208 | "/**$nl $decl$nl */$nl$1",
209 | $content,
210 | );
211 | } elseif (!preg_match('/@property(-read)?\s+.*\$template/', $doc)) {
212 | $newDoc = preg_replace('~(\s*)\*/\s*$~', "$1$decl$0", $doc, 1);
213 | $content = str_replace($doc, $newDoc, $content);
214 | } else {
215 | return;
216 | }
217 |
218 | file_put_contents($rc->getFileName(), $content);
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationTracy/dist/panel.phtml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
82 |
83 |
84 | no route
85 | = Tracy\Helpers::escapeHtml($matched[Presenter::PresenterKey]) ?>
86 | := Tracy\Helpers::escapeHtml($matched[Presenter::ActionKey] ?? Presenter::DefaultAction) ?>
87 |
88 |
89 | = Tracy\Helpers::escapeHtml($matched[Presenter::SignalKey]) ?>
90 | !
91 |
92 |
93 |
94 |
95 |
110 |
111 |
112 |
No routes defined.
113 |
114 |
115 |
116 |
Mask / Class
117 |
Defaults
118 |
Matched as
119 |
120 |
121 |
122 |
132 |
133 |
134 |
136 |
138 | = Tracy\Helpers::escapeHtml(['yes' => '✓', 'may' => '≈', 'no' => '', 'oneway' => '⛔', 'error' => '❌'][$route->matched]) ?>
139 |
140 |
141 |
142 |
143 |
145 |
146 | = Tracy\Helpers::escapeHtml($path) ?>
147 |
148 |
149 | = isset($route->mask) ? str_replace(['/', '-'], ['/', '-'], htmlspecialchars($route->mask)) : str_replace('\\', '\\', htmlspecialchars($route->class)) ?>
150 |
151 |
152 |
153 |
154 |
155 |
156 | defaults as $key => $value): ?> = Tracy\Helpers::escapeHtml($key) ?>
157 | =
158 | = Tracy\Helpers::escapeHtml($value) ?>
159 |
= Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) ?>
160 |
161 |
162 |
163 |
164 |
165 |
166 | params): ?>
167 | params ?> = Tracy\Helpers::escapeHtml($params['presenter']) ?>
168 | := Tracy\Helpers::escapeHtml($params[Presenter::ActionKey] ?? Presenter::DefaultAction) ?>
169 |
170 |
171 | $value): ?> = Tracy\Helpers::escapeHtml($key) ?>
172 | =
173 | = Tracy\Helpers::escapeHtml($value) ?>
174 |
= Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) ?>
175 |
176 |
177 |
178 | error): ?> = Tracy\Helpers::escapeHtml($route->error->getMessage()) ?>
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/src/Application/Application.php:
--------------------------------------------------------------------------------
1 | Occurs before the application loads presenter */
31 | public array $onStartup = [];
32 |
33 | /** @var array Occurs before the application shuts down */
34 | public array $onShutdown = [];
35 |
36 | /** @var array Occurs when a new request is received */
37 | public array $onRequest = [];
38 |
39 | /** @var array Occurs when a presenter is created */
40 | public array $onPresenter = [];
41 |
42 | /** @var array Occurs when a new response is ready for dispatch */
43 | public array $onResponse = [];
44 |
45 | /** @var array Occurs when an unhandled exception occurs in the application */
46 | public array $onError = [];
47 |
48 | /** @var Request[] */
49 | private array $requests = [];
50 | private ?IPresenter $presenter = null;
51 | private Nette\Http\IRequest $httpRequest;
52 | private Nette\Http\IResponse $httpResponse;
53 | private IPresenterFactory $presenterFactory;
54 | private Router $router;
55 |
56 |
57 | public function __construct(
58 | IPresenterFactory $presenterFactory,
59 | Router $router,
60 | Nette\Http\IRequest $httpRequest,
61 | Nette\Http\IResponse $httpResponse,
62 | ) {
63 | $this->httpRequest = $httpRequest;
64 | $this->httpResponse = $httpResponse;
65 | $this->presenterFactory = $presenterFactory;
66 | $this->router = $router;
67 | }
68 |
69 |
70 | /**
71 | * Dispatch a HTTP request to a front controller.
72 | */
73 | public function run(): void
74 | {
75 | try {
76 | Arrays::invoke($this->onStartup, $this);
77 | $this->processRequest($this->createInitialRequest())
78 | ->send($this->httpRequest, $this->httpResponse);
79 | Arrays::invoke($this->onShutdown, $this);
80 |
81 | } catch (\Throwable $e) {
82 | $this->sendHttpCode($e);
83 | Arrays::invoke($this->onError, $this, $e);
84 | if ($this->catchExceptions && ($req = $this->createErrorRequest($e))) {
85 | try {
86 | $this->processRequest($req)
87 | ->send($this->httpRequest, $this->httpResponse);
88 | Arrays::invoke($this->onShutdown, $this, $e);
89 | return;
90 |
91 | } catch (\Throwable $e) {
92 | Arrays::invoke($this->onError, $this, $e);
93 | }
94 | }
95 |
96 | Arrays::invoke($this->onShutdown, $this, $e);
97 | throw $e;
98 | }
99 | }
100 |
101 |
102 | public function createInitialRequest(): Request
103 | {
104 | $params = $this->router->match($this->httpRequest);
105 | $presenter = $params[UI\Presenter::PresenterKey] ?? null;
106 |
107 | if ($params === null) {
108 | throw new BadRequestException('No route for HTTP request.');
109 | } elseif (!is_string($presenter)) {
110 | throw new Nette\InvalidStateException('Missing presenter in route definition.');
111 | } elseif (str_starts_with($presenter, 'Nette:') && $presenter !== 'Nette:Micro') {
112 | throw new BadRequestException('Invalid request. Presenter is not achievable.');
113 | }
114 |
115 | unset($params[UI\Presenter::PresenterKey]);
116 | return new Request(
117 | $presenter,
118 | $this->httpRequest->getMethod(),
119 | $params,
120 | $this->httpRequest->getPost(),
121 | $this->httpRequest->getFiles(),
122 | );
123 | }
124 |
125 |
126 | public function processRequest(Request $request): Response
127 | {
128 | process:
129 | if (count($this->requests) > $this->maxLoop) {
130 | throw new ApplicationException('Too many loops detected in application life cycle.');
131 | }
132 |
133 | $this->requests[] = $request;
134 | Arrays::invoke($this->onRequest, $this, $request);
135 |
136 | if (
137 | !$request->isMethod($request::FORWARD)
138 | && (!strcasecmp($request->getPresenterName(), (string) $this->errorPresenter)
139 | || !strcasecmp($request->getPresenterName(), (string) $this->error4xxPresenter))
140 | ) {
141 | throw new BadRequestException('Invalid request. Presenter is not achievable.');
142 | }
143 |
144 | try {
145 | $this->presenter = $this->presenterFactory->createPresenter($request->getPresenterName());
146 | } catch (InvalidPresenterException $e) {
147 | throw count($this->requests) > 1
148 | ? $e
149 | : new BadRequestException($e->getMessage(), 0, $e);
150 | }
151 |
152 | Arrays::invoke($this->onPresenter, $this, $this->presenter);
153 | $response = $this->presenter->run(clone $request);
154 |
155 | if ($response instanceof Responses\ForwardResponse) {
156 | $request = $response->getRequest();
157 | goto process;
158 | }
159 |
160 | Arrays::invoke($this->onResponse, $this, $response);
161 | return $response;
162 | }
163 |
164 |
165 | public function createErrorRequest(\Throwable $e): ?Request
166 | {
167 | $errorPresenter = $e instanceof BadRequestException
168 | ? $this->error4xxPresenter ?? $this->errorPresenter
169 | : $this->errorPresenter;
170 |
171 | if ($errorPresenter === null) {
172 | return null;
173 | }
174 |
175 | $args = ['exception' => $e, 'previousPresenter' => $this->presenter, 'request' => Arrays::last($this->requests) ?? null];
176 | if ($this->presenter instanceof UI\Presenter) {
177 | try {
178 | $this->presenter->forward(":$errorPresenter:", $args);
179 | } catch (AbortException) {
180 | return $this->presenter->getLastCreatedRequest();
181 | }
182 | }
183 |
184 | return new Request($errorPresenter, Request::FORWARD, $args);
185 | }
186 |
187 |
188 | private function sendHttpCode(\Throwable $e): void
189 | {
190 | if (!$e instanceof BadRequestException && $this->httpResponse instanceof Nette\Http\Response) {
191 | $this->httpResponse->warnOnBuffer = false;
192 | }
193 |
194 | if (!$this->httpResponse->isSent()) {
195 | $this->httpResponse->setCode($e instanceof BadRequestException ? ($e->getHttpCode() ?: 404) : 500);
196 | }
197 | }
198 |
199 |
200 | /**
201 | * Returns all processed requests.
202 | * @return Request[]
203 | */
204 | final public function getRequests(): array
205 | {
206 | return $this->requests;
207 | }
208 |
209 |
210 | /**
211 | * Returns current presenter.
212 | */
213 | final public function getPresenter(): ?IPresenter
214 | {
215 | return $this->presenter;
216 | }
217 |
218 |
219 | /********************* services ****************d*g**/
220 |
221 |
222 | /**
223 | * Returns router.
224 | */
225 | public function getRouter(): Router
226 | {
227 | return $this->router;
228 | }
229 |
230 |
231 | /**
232 | * Returns presenter factory.
233 | */
234 | public function getPresenterFactory(): IPresenterFactory
235 | {
236 | return $this->presenterFactory;
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/Bridges/ApplicationDI/ApplicationExtension.php:
--------------------------------------------------------------------------------
1 | scanDirs = (array) $scanDirs;
40 | }
41 |
42 |
43 | public function getConfigSchema(): Nette\Schema\Schema
44 | {
45 | return Expect::structure([
46 | 'debugger' => Expect::bool(),
47 | 'errorPresenter' => Expect::anyOf(
48 | Expect::structure([
49 | '4xx' => Expect::string('Nette:Error')->dynamic(),
50 | '5xx' => Expect::string('Nette:Error')->dynamic(),
51 | ])->castTo('array'),
52 | Expect::string()->dynamic(),
53 | )->firstIsDefault(),
54 | 'catchExceptions' => Expect::bool(false)->dynamic(),
55 | 'mapping' => Expect::anyOf(
56 | Expect::string(),
57 | Expect::arrayOf('string|array'),
58 | ),
59 | 'aliases' => Expect::arrayOf('string'),
60 | 'scanDirs' => Expect::anyOf(
61 | Expect::arrayOf('string')->default($this->scanDirs)->mergeDefaults(),
62 | false,
63 | )->firstIsDefault(),
64 | 'scanComposer' => Expect::bool(class_exists(ClassLoader::class)),
65 | 'scanFilter' => Expect::string('*Presenter'),
66 | 'silentLinks' => Expect::bool(),
67 | 'generateTemplateClasses' => Expect::bool(),
68 | ]);
69 | }
70 |
71 |
72 | public function loadConfiguration(): void
73 | {
74 | $config = $this->config;
75 | $builder = $this->getContainerBuilder();
76 | $builder->addExcludedClasses([UI\Presenter::class]);
77 |
78 | $this->invalidLinkMode = $this->debugMode
79 | ? UI\Presenter::InvalidLinkTextual | ($config->silentLinks ? 0 : UI\Presenter::InvalidLinkWarning)
80 | : UI\Presenter::InvalidLinkWarning;
81 |
82 | $application = $builder->addDefinition($this->prefix('application'))
83 | ->setFactory(Nette\Application\Application::class);
84 | if ($config->catchExceptions || !$this->debugMode) {
85 | $application->addSetup('$error4xxPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['4xx'] : $config->errorPresenter]);
86 | $application->addSetup('$errorPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['5xx'] : $config->errorPresenter]);
87 | }
88 |
89 | $this->compiler->addExportedType(Nette\Application\Application::class);
90 |
91 | if ($this->debugMode && ($config->scanDirs || $this->robotLoader) && $this->tempDir) {
92 | $touch = $this->tempDir . '/touch';
93 | Nette\Utils\FileSystem::createDir($this->tempDir);
94 | $this->getContainerBuilder()->addDependency($touch);
95 | }
96 |
97 | $presenterFactory = $builder->addDefinition($this->prefix('presenterFactory'))
98 | ->setType(Nette\Application\IPresenterFactory::class)
99 | ->setFactory(Nette\Application\PresenterFactory::class, [new Definitions\Statement(
100 | Nette\Bridges\ApplicationDI\PresenterFactoryCallback::class,
101 | [1 => $touch ?? null],
102 | )]);
103 |
104 | if ($config->mapping) {
105 | $presenterFactory->addSetup('setMapping', [
106 | is_string($config->mapping) ? ['*' => $config->mapping] : $config->mapping,
107 | ]);
108 | }
109 |
110 | if ($config->aliases) {
111 | $presenterFactory->addSetup('setAliases', [$config->aliases]);
112 | }
113 |
114 | $builder->addDefinition($this->prefix('linkGenerator'))
115 | ->setFactory(Nette\Application\LinkGenerator::class, [
116 | 1 => new Definitions\Statement([new Definitions\Statement('@Nette\Http\IRequest::getUrl'), 'withoutUserInfo']),
117 | ]);
118 |
119 | if ($this->name === 'application') {
120 | $builder->addAlias('application', $this->prefix('application'));
121 | $builder->addAlias('nette.presenterFactory', $this->prefix('presenterFactory'));
122 | }
123 | }
124 |
125 |
126 | public function beforeCompile(): void
127 | {
128 | $builder = $this->getContainerBuilder();
129 |
130 | if ($this->config->debugger ?? $builder->getByType(Tracy\BlueScreen::class)) {
131 | $builder->getDefinition($this->prefix('application'))
132 | ->addSetup([self::class, 'initializeBlueScreenPanel']);
133 | }
134 |
135 | if ($this->debugMode && $this->config->generateTemplateClasses) {
136 | $builder->getDefinition('latte.templateFactory')
137 | ->setArgument('generate', true);
138 | }
139 |
140 | $all = [];
141 |
142 | foreach ($builder->findByType(Nette\Application\IPresenter::class) as $def) {
143 | $all[$def->getType()] = $def;
144 | }
145 |
146 | $counter = 0;
147 | foreach ($this->findPresenters() as $class) {
148 | $this->checkPresenter($class);
149 | if (empty($all[$class])) {
150 | $all[$class] = $builder->addDefinition($this->prefix((string) ++$counter))
151 | ->setType($class);
152 | }
153 | }
154 |
155 | foreach ($all as $def) {
156 | $def->addTag(Nette\DI\Extensions\InjectExtension::TagInject)
157 | ->setAutowired(false);
158 |
159 | if (is_subclass_of($def->getType(), UI\Presenter::class) && $def instanceof Definitions\ServiceDefinition) {
160 | $def->addSetup('$invalidLinkMode', [$this->invalidLinkMode]);
161 | }
162 |
163 | $this->compiler->addExportedType($def->getType());
164 | }
165 | }
166 |
167 |
168 | /** @return string[] */
169 | private function findPresenters(): array
170 | {
171 | $config = $this->getConfig();
172 |
173 | if ($config->scanDirs) {
174 | if (!class_exists(Nette\Loaders\RobotLoader::class)) {
175 | throw new Nette\NotSupportedException("RobotLoader is required to find presenters, install package `nette/robot-loader` or disable option {$this->prefix('scanDirs')}: false");
176 | }
177 |
178 | $robot = new Nette\Loaders\RobotLoader;
179 | $robot->addDirectory(...$config->scanDirs);
180 | $robot->acceptFiles = [$config->scanFilter . '.php'];
181 | if ($this->tempDir) {
182 | $robot->setTempDirectory($this->tempDir);
183 | $robot->refresh();
184 | } else {
185 | $robot->rebuild();
186 | }
187 | } elseif ($this->robotLoader && $config->scanDirs !== false) {
188 | $robot = $this->robotLoader;
189 | $robot->refresh();
190 | }
191 |
192 | $classes = [];
193 | if (isset($robot)) {
194 | $classes = array_keys($robot->getIndexedClasses());
195 | }
196 |
197 | if ($config->scanComposer) {
198 | $rc = new \ReflectionClass(ClassLoader::class);
199 | $classFile = dirname($rc->getFileName()) . '/autoload_classmap.php';
200 | if (is_file($classFile)) {
201 | $this->getContainerBuilder()->addDependency($classFile);
202 | $classes = array_merge($classes, array_keys((fn($path) => require $path)($classFile)));
203 | }
204 | }
205 |
206 | $presenters = [];
207 | foreach (array_unique($classes) as $class) {
208 | if (
209 | fnmatch($config->scanFilter, $class)
210 | && class_exists($class)
211 | && ($rc = new \ReflectionClass($class))
212 | && $rc->implementsInterface(Nette\Application\IPresenter::class)
213 | && !$rc->isAbstract()
214 | ) {
215 | $presenters[] = $rc->getName();
216 | }
217 | }
218 |
219 | return $presenters;
220 | }
221 |
222 |
223 | /** @internal */
224 | public static function initializeBlueScreenPanel(
225 | Tracy\BlueScreen $blueScreen,
226 | Nette\Application\Application $application,
227 | ): void
228 | {
229 | $blueScreen->addPanel(function (?\Throwable $e) use ($application, $blueScreen): ?array {
230 | $dumper = $blueScreen->getDumper();
231 | return $e ? null : [
232 | 'tab' => 'Nette Application',
233 | 'panel' => 'Requests
' . $dumper($application->getRequests())
234 | . 'Presenter
' . $dumper($application->getPresenter()),
235 | ];
236 | });
237 | if (
238 | version_compare(Tracy\Debugger::Version, '2.9.0', '>=')
239 | && version_compare(Tracy\Debugger::Version, '3.0', '<')
240 | ) {
241 | $blueScreen->addFileGenerator(self::generateNewPresenterFileContents(...));
242 | }
243 | }
244 |
245 |
246 | public static function generateNewPresenterFileContents(string $file, ?string $class = null): ?string
247 | {
248 | if (!$class || !str_ends_with($file, 'Presenter.php')) {
249 | return null;
250 | }
251 |
252 | $res = "checked[$class])) {
266 | return;
267 | }
268 | $this->checked[$class] = true;
269 |
270 | $rc = new \ReflectionClass($class);
271 | if ($rc->getParentClass()) {
272 | $this->checkPresenter($rc->getParentClass()->getName());
273 | }
274 |
275 | foreach ($rc->getProperties() as $rp) {
276 | if (($rp->getAttributes($attr = Attributes\Parameter::class) || $rp->getAttributes($attr = Attributes\Persistent::class))
277 | && (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly())
278 | ) {
279 | throw new Nette\InvalidStateException(sprintf('Property %s: attribute %s can be used only with public non-static property.', Reflection::toString($rp), $attr));
280 | }
281 | }
282 |
283 | $re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.';
284 | foreach ($rc->getMethods() as $rm) {
285 | if (preg_match("#^$re#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) {
286 | throw new Nette\InvalidStateException(sprintf('Method %s: this method must be public non-static.', Reflection::toString($rm)));
287 | } elseif (preg_match('#^createComponent.#', $rm->getName()) && ($rm->isPrivate() || $rm->isStatic())) {
288 | throw new Nette\InvalidStateException(sprintf('Method %s: this method must be non-private non-static.', Reflection::toString($rm)));
289 | } elseif ($rm->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF)
290 | && !preg_match("#^$re|createComponent.#", $rm->getName())
291 | ) {
292 | 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));
293 | } elseif ($rm->getAttributes(Attributes\Deprecated::class) && !preg_match("#^$re#", $rm->getName())) {
294 | 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));
295 | }
296 | }
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/src/Application/LinkGenerator.php:
--------------------------------------------------------------------------------
1 | createRequest($component, $parts['path'] . ($parts['signal'] ? '!' : ''), $args, $mode ?? 'link');
50 | $relative = $mode === 'link' && !$parts['absolute'] && !$component?->getPresenter()->absoluteUrls;
51 | return $mode === 'forward' || $mode === 'test'
52 | ? null
53 | : $this->requestToUrl($request, $relative) . $parts['fragment'];
54 | }
55 |
56 |
57 | /**
58 | * @param string $destination in format "[[[module:]presenter:]action | signal! | this | @alias]"
59 | * @param string $mode forward|redirect|link
60 | * @throws UI\InvalidLinkException
61 | * @internal
62 | */
63 | public function createRequest(
64 | ?UI\Component $component,
65 | string $destination,
66 | array $args,
67 | string $mode,
68 | ): Request
69 | {
70 | // note: createRequest supposes that saveState(), run() & tryCall() behaviour is final
71 |
72 | $this->lastRequest = null;
73 | $refPresenter = $component?->getPresenter();
74 | $path = $destination;
75 |
76 | if (($component && !$component instanceof UI\Presenter) || str_ends_with($destination, '!')) {
77 | [$cname, $signal] = Helpers::splitName(rtrim($destination, '!'));
78 | if ($cname !== '') {
79 | $component = $component->getComponent(strtr($cname, ':', '-'));
80 | }
81 |
82 | if ($signal === '') {
83 | throw new UI\InvalidLinkException('Signal must be non-empty string.');
84 | }
85 |
86 | $path = 'this';
87 | }
88 |
89 | if ($path[0] === '@') {
90 | if (!$this->presenterFactory instanceof PresenterFactory) {
91 | throw new \LogicException('Link aliasing requires PresenterFactory service.');
92 | }
93 | $path = ':' . $this->presenterFactory->getAlias(substr($path, 1));
94 | }
95 |
96 | $current = false;
97 | [$presenter, $action] = Helpers::splitName($path);
98 | if ($presenter === '') {
99 | if (!$refPresenter) {
100 | throw new \LogicException("Presenter must be specified in '$destination'.");
101 | }
102 | $action = $path === 'this' ? $refPresenter->getAction() : $action;
103 | $presenter = $refPresenter->getName();
104 | $presenterClass = $refPresenter::class;
105 |
106 | } else {
107 | if ($presenter[0] === ':') { // absolute
108 | $presenter = substr($presenter, 1);
109 | if (!$presenter) {
110 | throw new UI\InvalidLinkException("Missing presenter name in '$destination'.");
111 | }
112 | } elseif ($refPresenter) { // relative
113 | [$module, , $sep] = Helpers::splitName($refPresenter->getName());
114 | $presenter = $module . $sep . $presenter;
115 | }
116 |
117 | try {
118 | $presenterClass = $this->presenterFactory?->getPresenterClass($presenter);
119 | } catch (InvalidPresenterException $e) {
120 | throw new UI\InvalidLinkException($e->getMessage(), 0, $e);
121 | }
122 | }
123 |
124 | // PROCESS SIGNAL ARGUMENTS
125 | if (isset($signal)) { // $component must be StatePersistent
126 | $reflection = new UI\ComponentReflection($component::class);
127 | if ($signal === 'this') { // means "no signal"
128 | $signal = '';
129 | if (array_key_exists(0, $args)) {
130 | throw new UI\InvalidLinkException("Unable to pass parameters to 'this!' signal.");
131 | }
132 | } elseif (!str_contains($signal, UI\Component::NameSeparator)) {
133 | // counterpart of signalReceived() & tryCall()
134 |
135 | $method = $reflection->getSignalMethod($signal);
136 | if (!$method) {
137 | throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()");
138 | }
139 |
140 | $this->checkAllowed($refPresenter, $method, "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class), $mode);
141 |
142 | // convert indexed parameters to named
143 | UI\ParameterConverter::toParameters($method, $args, [], $missing);
144 | }
145 |
146 | // counterpart of StatePersistent
147 | if ($args && array_intersect_key($args, $reflection->getPersistentParams())) {
148 | $component->saveState($args);
149 | }
150 |
151 | if ($args && $component !== $refPresenter) {
152 | $prefix = $component->getUniqueId() . UI\Component::NameSeparator;
153 | foreach ($args as $key => $val) {
154 | unset($args[$key]);
155 | $args[$prefix . $key] = $val;
156 | }
157 | }
158 | }
159 |
160 | // PROCESS ARGUMENTS
161 | if (is_subclass_of($presenterClass, UI\Presenter::class)) {
162 | if ($action === '') {
163 | $action = UI\Presenter::DefaultAction;
164 | }
165 |
166 | $current = $refPresenter && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) && $presenterClass === $refPresenter::class;
167 |
168 | $reflection = new UI\ComponentReflection($presenterClass);
169 | $this->checkAllowed($refPresenter, $reflection, "presenter '$presenter'", $mode);
170 |
171 | foreach (array_intersect_key($reflection->getParameters(), $args) as $name => $param) {
172 | if ($args[$name] === $param['def']) {
173 | $args[$name] = null; // value transmit is unnecessary
174 | }
175 | }
176 |
177 | // counterpart of run() & tryCall()
178 | if ($method = $reflection->getActionRenderMethod($action)) {
179 | $this->checkAllowed($refPresenter, $method, "action '$presenter:$action'", $mode);
180 |
181 | UI\ParameterConverter::toParameters($method, $args, $path === 'this' ? $refPresenter->getParameters() : [], $missing);
182 |
183 | } elseif (array_key_exists(0, $args)) {
184 | throw new UI\InvalidLinkException("Unable to pass parameters to action '$presenter:$action', missing corresponding method $presenterClass::{$presenterClass::formatRenderMethod($action)}().");
185 | }
186 |
187 | // counterpart of StatePersistent
188 | if ($refPresenter) {
189 | if (empty($signal) && $args && array_intersect_key($args, $reflection->getPersistentParams())) {
190 | $refPresenter->saveStatePartial($args, $reflection);
191 | }
192 |
193 | $globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass);
194 | if ($current && $args) {
195 | $tmp = $globalState + $refPresenter->getParameters();
196 | foreach ($args as $key => $val) {
197 | if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) {
198 | $current = false;
199 | break;
200 | }
201 | }
202 | }
203 |
204 | $args += $globalState;
205 | }
206 | }
207 |
208 | if ($mode !== 'test' && !empty($missing)) {
209 | foreach ($missing as $rp) {
210 | if (!array_key_exists($rp->getName(), $args)) {
211 | throw new UI\InvalidLinkException("Missing parameter \${$rp->getName()} required by " . Reflection::toString($rp->getDeclaringFunction()));
212 | }
213 | }
214 | }
215 |
216 | // ADD ACTION & SIGNAL & FLASH
217 | if ($action) {
218 | $args[UI\Presenter::ActionKey] = $action;
219 | }
220 |
221 | if (!empty($signal)) {
222 | $args[UI\Presenter::SignalKey] = $component->getParameterId($signal);
223 | $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter->getParameter(UI\Presenter::SignalKey);
224 | }
225 |
226 | if (($mode === 'redirect' || $mode === 'forward') && $refPresenter->hasFlashSession()) {
227 | $flashKey = $refPresenter->getParameter(UI\Presenter::FlashKey);
228 | $args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null;
229 | }
230 |
231 | return $this->lastRequest = new Request($presenter, Request::FORWARD, $args, flags: ['current' => $current]);
232 | }
233 |
234 |
235 | /**
236 | * Parse destination in format "[//] [[[module:]presenter:]action | signal! | this | @alias] [?query] [#fragment]"
237 | * @throws UI\InvalidLinkException
238 | * @return array{absolute: bool, path: string, signal: bool, args: ?array, fragment: string}
239 | * @internal
240 | */
241 | public static function parseDestination(string $destination): array
242 | {
243 | if (!preg_match('~^ (?//)?+ (?[^!?#]++) (?!)?+ (?\?[^#]*)?+ (?\#.*)?+ $~x', $destination, $matches)) {
244 | throw new UI\InvalidLinkException("Invalid destination '$destination'.");
245 | }
246 |
247 | if (!empty($matches['query'])) {
248 | trigger_error("Link format is obsolete, use arguments instead of query string in '$destination'.", E_USER_DEPRECATED);
249 | parse_str(substr($matches['query'], 1), $args);
250 | }
251 |
252 | return [
253 | 'absolute' => (bool) $matches['absolute'],
254 | 'path' => $matches['path'],
255 | 'signal' => !empty($matches['signal']),
256 | 'args' => $args ?? null,
257 | 'fragment' => $matches['fragment'] ?? '',
258 | ];
259 | }
260 |
261 |
262 | /**
263 | * Converts Request to URL.
264 | */
265 | public function requestToUrl(Request $request, ?bool $relative = false): string
266 | {
267 | $url = $this->router->constructUrl($request->toArray(), $this->refUrl);
268 | if ($url === null) {
269 | $params = $request->getParameters();
270 | unset($params[UI\Presenter::ActionKey], $params[UI\Presenter::PresenterKey]);
271 | $params = urldecode(http_build_query($params, '', ', '));
272 | throw new UI\InvalidLinkException("No route for {$request->getPresenterName()}:{$request->getParameter('action')}($params)");
273 | }
274 |
275 | if ($relative) {
276 | $hostUrl = $this->refUrl->getHostUrl() . '/';
277 | if (strncmp($url, $hostUrl, strlen($hostUrl)) === 0) {
278 | $url = substr($url, strlen($hostUrl) - 1);
279 | }
280 | }
281 |
282 | return $url;
283 | }
284 |
285 |
286 | public function withReferenceUrl(string $url): static
287 | {
288 | return new self(
289 | $this->router,
290 | new UrlScript($url),
291 | $this->presenterFactory,
292 | );
293 | }
294 |
295 |
296 | private function checkAllowed(
297 | ?UI\Presenter $presenter,
298 | \ReflectionClass|\ReflectionMethod $element,
299 | string $message,
300 | string $mode,
301 | ): void
302 | {
303 | if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->canGenerateLink()) {
304 | throw new UI\InvalidLinkException("Link to forbidden $message from '{$presenter->getName()}:{$presenter->getAction()}'.");
305 | } elseif ($presenter?->invalidLinkMode && $element->getAttributes(Attributes\Deprecated::class)) {
306 | trigger_error("Link to deprecated $message from '{$presenter->getName()}:{$presenter->getAction()}'.", E_USER_DEPRECATED);
307 | }
308 | }
309 |
310 |
311 | /** @internal */
312 | public static function applyBase(string $link, string $base): string
313 | {
314 | return str_contains($link, ':') && $link[0] !== ':'
315 | ? ":$base:$link"
316 | : $link;
317 | }
318 | }
319 |
--------------------------------------------------------------------------------
/src/Application/UI/Component.php:
--------------------------------------------------------------------------------
1 | Occurs when component is attached to presenter */
31 | public array $onAnchor = [];
32 | protected array $params = [];
33 |
34 |
35 | /**
36 | * Returns the presenter where this component belongs to.
37 | */
38 | public function getPresenter(): ?Presenter
39 | {
40 | if (func_num_args()) {
41 | trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getPresenterIfExists()', E_USER_DEPRECATED);
42 | $throw = func_get_arg(0);
43 | }
44 |
45 | return $this->lookup(Presenter::class, throw: $throw ?? true);
46 | }
47 |
48 |
49 | /**
50 | * Returns the presenter where this component belongs to.
51 | */
52 | public function getPresenterIfExists(): ?Presenter
53 | {
54 | return $this->lookup(Presenter::class, throw: false);
55 | }
56 |
57 |
58 | /** @deprecated */
59 | public function hasPresenter(): bool
60 | {
61 | return (bool) $this->lookup(Presenter::class, throw: false);
62 | }
63 |
64 |
65 | /**
66 | * Returns a fully-qualified name that uniquely identifies the component
67 | * within the presenter hierarchy.
68 | */
69 | public function getUniqueId(): string
70 | {
71 | return $this->lookupPath(Presenter::class);
72 | }
73 |
74 |
75 | public function addComponent(
76 | Nette\ComponentModel\IComponent $component,
77 | ?string $name,
78 | ?string $insertBefore = null,
79 | ): static
80 | {
81 | if (!$component instanceof SignalReceiver && !$component instanceof StatePersistent) {
82 | throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' is not intended to be used in the Presenter.');
83 | }
84 |
85 | return parent::addComponent($component, $name, $insertBefore = null);
86 | }
87 |
88 |
89 | protected function createComponent(string $name): ?Nette\ComponentModel\IComponent
90 | {
91 | if (method_exists($this, $method = 'createComponent' . $name)) {
92 | (new AccessPolicy(new \ReflectionMethod($this, $method)))->checkAccess($this);
93 | $this->checkRequirements($rm);
94 | }
95 | return parent::createComponent($name);
96 | }
97 |
98 |
99 | protected function validateParent(Nette\ComponentModel\IContainer $parent): void
100 | {
101 | parent::validateParent($parent);
102 | $this->monitor(Presenter::class, function (Presenter $presenter): void {
103 | $this->loadState($presenter->popGlobalParameters($this->getUniqueId()));
104 | Nette\Utils\Arrays::invoke($this->onAnchor, $this);
105 | });
106 | }
107 |
108 |
109 | /**
110 | * Calls public method if exists.
111 | */
112 | protected function tryCall(string $method, array $params): bool
113 | {
114 | $rc = $this->getReflection();
115 | if (!$rc->hasMethod($method)) {
116 | return false;
117 | } elseif (!$rc->hasCallableMethod($method)) {
118 | $this->error('Method ' . Nette\Utils\Reflection::toString($rc->getMethod($method)) . ' is not callable.');
119 | }
120 |
121 | $rm = $rc->getMethod($method);
122 | (new AccessPolicy($rm))->checkAccess($this);
123 | $this->checkRequirements($rm);
124 | try {
125 | $args = ParameterConverter::toArguments($rm, $params);
126 | } catch (Nette\InvalidArgumentException $e) {
127 | $this->error($e->getMessage());
128 | }
129 |
130 | $rm->invokeArgs($this, $args);
131 | return true;
132 | }
133 |
134 |
135 | /**
136 | * Descendant can override this method to check for permissions.
137 | * It is called with the presenter class and the render*(), action*(), and handle*() methods.
138 | */
139 | public function checkRequirements(\ReflectionClass|\ReflectionMethod $element): void
140 | {
141 | }
142 |
143 |
144 | /**
145 | * Access to reflection.
146 | */
147 | public static function getReflection(): ComponentReflection
148 | {
149 | return new ComponentReflection(static::class);
150 | }
151 |
152 |
153 | /********************* interface StatePersistent ****************d*g**/
154 |
155 |
156 | /**
157 | * Loads state information.
158 | */
159 | public function loadState(array $params): void
160 | {
161 | $reflection = $this->getReflection();
162 | foreach ($reflection->getParameters() as $name => $meta) {
163 | if (isset($params[$name])) { // nulls are ignored
164 | if (!ParameterConverter::convertType($params[$name], $meta['type'])) {
165 | $this->error(sprintf(
166 | "Value passed to persistent parameter '%s' in %s must be %s, %s given.",
167 | $name,
168 | $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'",
169 | $meta['type'],
170 | get_debug_type($params[$name]),
171 | ));
172 | }
173 |
174 | $this->$name = &$params[$name];
175 | } else {
176 | $params[$name] = &$this->$name;
177 | }
178 | }
179 |
180 | $this->params = $params;
181 | }
182 |
183 |
184 | /**
185 | * Saves state information for next request.
186 | */
187 | public function saveState(array &$params): void
188 | {
189 | $this->saveStatePartial($params, static::getReflection());
190 | }
191 |
192 |
193 | /**
194 | * @internal used by presenter
195 | */
196 | public function saveStatePartial(array &$params, ComponentReflection $reflection): void
197 | {
198 | $tree = Nette\Application\Helpers::getClassesAndTraits(static::class);
199 |
200 | foreach ($reflection->getPersistentParams() as $name => $meta) {
201 | if (isset($params[$name])) {
202 | // injected value
203 |
204 | } elseif (
205 | array_key_exists($name, $params) // nulls are skipped
206 | || (isset($meta['since']) && !isset($tree[$meta['since']])) // not related
207 | || !isset($this->$name)
208 | ) {
209 | continue;
210 |
211 | } else {
212 | $params[$name] = $this->$name; // object property value
213 | }
214 |
215 | if (!ParameterConverter::convertType($params[$name], $meta['type'])) {
216 | throw new InvalidLinkException(sprintf(
217 | "Value passed to persistent parameter '%s' in %s must be %s, %s given.",
218 | $name,
219 | $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'",
220 | $meta['type'],
221 | get_debug_type($params[$name]),
222 | ));
223 | }
224 |
225 | if ($params[$name] === $meta['def'] || ($meta['def'] === null && $params[$name] === '')) {
226 | $params[$name] = null; // value transmit is unnecessary
227 | }
228 | }
229 | }
230 |
231 |
232 | /**
233 | * Returns component param.
234 | */
235 | final public function getParameter(string $name): mixed
236 | {
237 | if (func_num_args() > 1) {
238 | trigger_error(__METHOD__ . '() parameter $default is deprecated, use operator ??', E_USER_DEPRECATED);
239 | $default = func_get_arg(1);
240 | }
241 | return $this->params[$name] ?? $default ?? null;
242 | }
243 |
244 |
245 | /**
246 | * Returns component parameters.
247 | */
248 | final public function getParameters(): array
249 | {
250 | return array_map(fn($item) => $item, $this->params);
251 | }
252 |
253 |
254 | /**
255 | * Returns a fully-qualified name that uniquely identifies the parameter.
256 | */
257 | final public function getParameterId(string $name): string
258 | {
259 | $uid = $this->getUniqueId();
260 | return $uid === '' ? $name : $uid . self::NameSeparator . $name;
261 | }
262 |
263 |
264 | /********************* interface SignalReceiver ****************d*g**/
265 |
266 |
267 | /**
268 | * Calls signal handler method.
269 | * @throws BadSignalException if there is not handler method
270 | */
271 | public function signalReceived(string $signal): void
272 | {
273 | if (!$this->tryCall($this->formatSignalMethod($signal), $this->params)) {
274 | $class = static::class;
275 | throw new BadSignalException("There is no handler for signal '$signal' in class $class.");
276 | }
277 | }
278 |
279 |
280 | /**
281 | * Formats signal handler method name -> case sensitivity doesn't matter.
282 | */
283 | public static function formatSignalMethod(string $signal): string
284 | {
285 | return 'handle' . $signal;
286 | }
287 |
288 |
289 | /********************* navigation ****************d*g**/
290 |
291 |
292 | /**
293 | * Generates URL to presenter, action or signal.
294 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]"
295 | * @param mixed ...$args
296 | * @throws InvalidLinkException
297 | */
298 | public function link(string $destination, ...$args): string
299 | {
300 | try {
301 | $args = count($args) === 1 && is_array($args[0] ?? null)
302 | ? $args[0]
303 | : $args;
304 | return $this->getPresenter()->getLinkGenerator()->link($destination, $args, $this, 'link');
305 |
306 | } catch (InvalidLinkException $e) {
307 | return $this->getPresenter()->processInvalidLink($e);
308 | }
309 | }
310 |
311 |
312 | /**
313 | * Returns destination as Link object.
314 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]"
315 | * @param mixed ...$args
316 | */
317 | public function lazyLink(string $destination, ...$args): Link
318 | {
319 | $args = count($args) === 1 && is_array($args[0] ?? null)
320 | ? $args[0]
321 | : $args;
322 | return new Link($this, $destination, $args);
323 | }
324 |
325 |
326 | /**
327 | * Determines whether it links to the current page.
328 | * @param ?string $destination in format "[[[module:]presenter:]action | signal! | this]"
329 | * @param mixed ...$args
330 | * @throws InvalidLinkException
331 | */
332 | public function isLinkCurrent(?string $destination = null, ...$args): bool
333 | {
334 | if ($destination !== null) {
335 | $args = count($args) === 1 && is_array($args[0] ?? null)
336 | ? $args[0]
337 | : $args;
338 | $this->getPresenter()->getLinkGenerator()->createRequest($this, $destination, $args, 'test');
339 | }
340 |
341 | return $this->getPresenter()->getLastCreatedRequestFlag('current');
342 | }
343 |
344 |
345 | /**
346 | * Redirect to another presenter, action or signal.
347 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]"
348 | * @param mixed ...$args
349 | * @throws Nette\Application\AbortException
350 | */
351 | public function redirect(string $destination, ...$args): never
352 | {
353 | $args = count($args) === 1 && is_array($args[0] ?? null)
354 | ? $args[0]
355 | : $args;
356 | $presenter = $this->getPresenter();
357 | $presenter->saveGlobalState();
358 | $presenter->redirectUrl($presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect'));
359 | }
360 |
361 |
362 | /**
363 | * Permanently redirects to presenter, action or signal.
364 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]"
365 | * @param mixed ...$args
366 | * @throws Nette\Application\AbortException
367 | */
368 | public function redirectPermanent(string $destination, ...$args): never
369 | {
370 | $args = count($args) === 1 && is_array($args[0] ?? null)
371 | ? $args[0]
372 | : $args;
373 | $presenter = $this->getPresenter();
374 | $presenter->redirectUrl(
375 | $presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect'),
376 | Nette\Http\IResponse::S301_MovedPermanently,
377 | );
378 | }
379 |
380 |
381 | /**
382 | * Throws HTTP error.
383 | * @throws Nette\Application\BadRequestException
384 | */
385 | public function error(string $message = '', int $httpCode = Nette\Http\IResponse::S404_NotFound): never
386 | {
387 | throw new Nette\Application\BadRequestException($message, $httpCode);
388 | }
389 | }
390 |
--------------------------------------------------------------------------------