Skip to content

Commit

Permalink
LinkGenerator: unified code
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Apr 16, 2024
1 parent 9b10b7a commit 0b89f40
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 94 deletions.
132 changes: 47 additions & 85 deletions src/Application/LinkGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,87 +33,48 @@ public function __construct(

/**
* Generates URL to presenter.
* @param string $dest in format "[[[module:]presenter:]action] [#fragment]"
* @param string $destination in format "[[[module:]presenter:]action | signal! | this | @alias]"
* @throws UI\InvalidLinkException
*/
public function link(string $dest, array $params = []): string
public function link(
string $destination,
array $args = [],
?UI\Component $component = null,
?string $mode = null,
): ?string
{
if (!preg_match('~^(@[\w:-]++|[\w:]+:\w*+)(#.*)?()$~D', $dest, $m)) {
throw new UI\InvalidLinkException("Invalid link destination '$dest'.");
}

[, $path, $frag] = $m;
if ($path[0] === '@') {
if (!$this->presenterFactory instanceof PresenterFactory) {
throw new Nette\InvalidStateException('Link aliasing requires PresenterFactory service.');
}
$path = $this->presenterFactory->getAlias(substr($path, 1));
}
[$presenter, $action] = Helpers::splitName($path);

try {
$class = $this->presenterFactory?->getPresenterClass($presenter);
} catch (InvalidPresenterException $e) {
throw new UI\InvalidLinkException($e->getMessage(), 0, $e);
}

if (is_subclass_of($class, UI\Presenter::class)) {
if ($action === '') {
$action = UI\Presenter::DefaultAction;
}

if ($method = $class::getReflection()->getActionRenderMethod($action)) {
self::argsToParams($class, $method->getName(), $params, [], $missing);
if ($missing) {
$rp = $missing[0];
throw new UI\InvalidLinkException("Missing parameter \${$rp->getName()} required by $class::{$method->getName()}()");
}
} elseif (array_key_exists(0, $params)) {
throw new UI\InvalidLinkException("Unable to pass parameters to action '$presenter:$action', missing corresponding method.");
}
}

if ($action !== '') {
$params[UI\Presenter::ActionKey] = $action;
}

$params[UI\Presenter::PresenterKey] = $presenter;

$url = $this->router->constructUrl($params, $this->refUrl);
if ($url === null) {
unset($params[UI\Presenter::ActionKey], $params[UI\Presenter::PresenterKey]);
$paramsDecoded = urldecode(http_build_query($params, '', ', '));
throw new UI\InvalidLinkException("No route for $dest($paramsDecoded)");
}

return $url . $frag;
$parts = static::parseDestination($destination);
$args = $parts['args'] ?? $args; // TODO: deprecate
$request = $this->createRequest($component, $parts['path'] . ($parts['signal'] ? '!' : ''), $args, $mode ?? 'link');
$relative = $mode === 'link' && !$parts['absolute'] && !$component?->getPresenter()->absoluteUrls;
return $mode === 'forward' || $mode === 'test'
? null
: $this->requestToUrl($request, $relative) . $parts['fragment'];
}



/**
* @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]"
* @param string $mode forward|redirect|link
* @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]"
* @param string $mode forward|redirect|link
* @throws UI\InvalidLinkException
* @internal
*/
public function createRequest(
UI\Component $component,
?UI\Component $component,
string $destination,
array $args,
string $mode,
): ?string
): Application\Request
{
// note: createRequest supposes that saveState(), run() & tryCall() behaviour is final

$this->lastRequest = null;
$refPresenter = $component->getPresenter();
$refPresenter = $component?->getPresenter();
$path = $destination;

$parts = static::parseDestination($destination);
$path = $parts['path'];
$args = $parts['args'] ?? $args;

if (!$component instanceof UI\Presenter || $parts['signal']) {
[$cname, $signal] = Helpers::splitName($path);
if (($component && !$component instanceof UI\Presenter) || str_ends_with($destination, '!')) {
[$cname, $signal] = Helpers::splitName(rtrim($destination, '!'));
if ($cname !== '') {
$component = $component->getComponent(strtr($cname, ':', '-'));
}
Expand All @@ -135,6 +96,9 @@ public function createRequest(
$current = false;
[$presenter, $action] = Helpers::splitName($path);
if ($presenter === '') {
if (!$refPresenter) {
throw new Nette\InvalidStateException("Presenter must be specified in '$destination'.");
}
$action = $path === 'this' ? $refPresenter->getAction() : $action;
$presenter = $refPresenter->getName();
$presenterClass = $refPresenter::class;
Expand All @@ -145,13 +109,13 @@ public function createRequest(
if (!$presenter) {
throw new UI\InvalidLinkException("Missing presenter name in '$destination'.");
}
} else { // relative
} elseif ($refPresenter) { // relative
[$module, , $sep] = Helpers::splitName($refPresenter->getName());
$presenter = $module . $sep . $presenter;
}

try {
$presenterClass = $this->presenterFactory->getPresenterClass($presenter);
$presenterClass = $this->presenterFactory?->getPresenterClass($presenter);
} catch (Application\InvalidPresenterException $e) {
throw new UI\InvalidLinkException($e->getMessage(), 0, $e);
}
Expand All @@ -172,7 +136,7 @@ public function createRequest(
if (!$method) {
throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()");
} elseif (
$refPresenter->invalidLinkMode
$refPresenter?->invalidLinkMode
&& (UI\ComponentReflection::parseAnnotation($method, 'deprecated') || $method->getAttributes(Attributes\Deprecated::class))
) {
trigger_error("Link to deprecated signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class) . " from '{$refPresenter->getName()}:{$refPresenter->getAction()}'.", E_USER_DEPRECATED);
Expand Down Expand Up @@ -202,10 +166,10 @@ public function createRequest(
$action = UI\Presenter::DefaultAction;
}

$current = ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) && $presenterClass === $refPresenter::class;
$current = $refPresenter && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) && $presenterClass === $refPresenter::class;

$reflection = new UI\ComponentReflection($presenterClass);
if ($refPresenter->invalidLinkMode
if ($refPresenter?->invalidLinkMode
&& (UI\ComponentReflection::parseAnnotation($reflection, 'deprecated') || $reflection->getAttributes(Attributes\Deprecated::class))
) {
trigger_error("Link to deprecated presenter '$presenter' from '{$refPresenter->getName()}:{$refPresenter->getAction()}'.", E_USER_DEPRECATED);
Expand All @@ -214,7 +178,7 @@ public function createRequest(
// counterpart of run() & tryCall()
if ($method = $reflection->getActionRenderMethod($action)) {
if (
$refPresenter->invalidLinkMode
$refPresenter?->invalidLinkMode
&& (UI\ComponentReflection::parseAnnotation($method, 'deprecated') || $method->getAttributes(Attributes\Deprecated::class))
) {
trigger_error("Link to deprecated action '$presenter:$action' from '{$refPresenter->getName()}:{$refPresenter->getAction()}'.", E_USER_DEPRECATED);
Expand All @@ -227,22 +191,24 @@ public function createRequest(
}

// counterpart of StatePersistent
if (empty($signal) && $args && array_intersect_key($args, $reflection->getPersistentParams())) {
$refPresenter->saveState($args, $reflection);
}
if ($refPresenter) {
if (empty($signal) && $args && array_intersect_key($args, $reflection->getPersistentParams())) {
$refPresenter->saveState($args, $reflection);
}

$globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass);
if ($current && $args) {
$tmp = $globalState + $refPresenter->getParameters();
foreach ($args as $key => $val) {
if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) {
$current = false;
break;
$globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass);
if ($current && $args) {
$tmp = $globalState + $refPresenter->getParameters();
foreach ($args as $key => $val) {
if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) {
$current = false;
break;
}
}
}
}

$args += $globalState;
$args += $globalState;
}
}

if ($mode !== 'test' && !empty($missing)) {
Expand All @@ -268,11 +234,7 @@ public function createRequest(
$args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null;
}

$this->lastRequest = new Application\Request($presenter, Application\Request::FORWARD, $args, flags: ['current' => $current]);

return $mode === 'forward' || $mode === 'test'
? null
: $this->requestToUrl($this->lastRequest, $mode === 'link' && !$parts['absolute'] && !$refPresenter->absoluteUrls) . $parts['fragment'];
return $this->lastRequest = new Application\Request($presenter, Application\Request::FORWARD, $args, flags: ['current' => $current]);
}


Expand Down
6 changes: 3 additions & 3 deletions src/Application/UI/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public function link(string $destination, $args = []): string
$args = func_num_args() < 3 && is_array($args)
? $args
: array_slice(func_get_args(), 1);
return $this->getPresenter()->getLinkGenerator()->createRequest($this, $destination, $args, 'link');
return $this->getPresenter()->getLinkGenerator()->link($destination, $args, $this, 'link');

} catch (InvalidLinkException $e) {
return $this->getPresenter()->handleInvalidLink($e);
Expand Down Expand Up @@ -305,7 +305,7 @@ public function redirect(string $destination, $args = []): void
: array_slice(func_get_args(), 1);
$presenter = $this->getPresenter();
$presenter->saveGlobalState();
$presenter->redirectUrl($presenter->getLinkGenerator()->createRequest($this, $destination, $args, 'redirect'));
$presenter->redirectUrl($presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect'));
}


Expand All @@ -323,7 +323,7 @@ public function redirectPermanent(string $destination, $args = []): void
: array_slice(func_get_args(), 1);
$presenter = $this->getPresenter();
$presenter->redirectUrl(
$presenter->getLinkGenerator()->createRequest($this, $destination, $args, 'redirect'),
$presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect'),
Nette\Http\IResponse::S301_MovedPermanently,
);
}
Expand Down
10 changes: 5 additions & 5 deletions src/Application/UI/Presenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -727,8 +727,8 @@ public function forward(string|Nette\Application\Request $destination, $args = [
$args = func_num_args() < 3 && is_array($args)
? $args
: array_slice(func_get_args(), 1);
$this->linkGenerator->createRequest($this, $destination, $args, 'forward');
$this->sendResponse(new Responses\ForwardResponse($this->linkGenerator->lastRequest));
$request = $this->linkGenerator->createRequest($this, $destination, $args, 'forward');
$this->sendResponse(new Responses\ForwardResponse($request));
}


Expand Down Expand Up @@ -789,10 +789,10 @@ public function canonicalize(?string $destination = null, ...$args): void
? $args[0]
: $args;
try {
$url = $this->linkGenerator->createRequest(
$this,
$url = $this->linkGenerator->link(
$destination ?: $this->action,
$args + $this->getGlobalState() + $request->getParameters(),
$this,
'redirectX',
);
} catch (InvalidLinkException) {
Expand Down Expand Up @@ -835,7 +835,7 @@ public function lastModified(
/** @deprecated @internal */
protected function createRequest(Component $component, string $destination, array $args, string $mode): ?string
{
return $this->linkGenerator->createRequest($component, $destination, $args, $mode);
return $this->linkGenerator->link($destination, $args, $component, $mode);
}


Expand Down
2 changes: 1 addition & 1 deletion tests/Routers/LinkGenerator.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ namespace {
Assert::exception(function () use ($pf) {
$generator = new LinkGenerator(new Routers\SimpleRouter, new Http\UrlScript('http://nette.org/en/'), $pf);
$generator->link('default');
}, Nette\Application\UI\InvalidLinkException::class, "Invalid link destination 'default'.");
}, Nette\InvalidStateException::class, "Presenter must be specified in 'default'.");


Assert::exception(function () use ($pf) {
Expand Down

0 comments on commit 0b89f40

Please sign in to comment.