-
-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
link generation moved to a new class UI\LinkBuilder
- Loading branch information
Showing
8 changed files
with
386 additions
and
316 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,339 @@ | ||
<?php | ||
|
||
/** | ||
* This file is part of the Nette Framework (https://nette.org) | ||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com) | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Nette\Application\UI; | ||
|
||
use Nette; | ||
use Nette\Application; | ||
use Nette\Application\Attributes; | ||
use Nette\Application\Helpers; | ||
use Nette\Http; | ||
|
||
|
||
/** @internal */ | ||
final class LinkBuilder | ||
{ | ||
public ?Nette\Application\Request $lastCreatedRequest; | ||
public ?array $lastCreatedRequestFlag; | ||
|
||
|
||
public function __construct( | ||
private readonly Presenter $presenter, | ||
private readonly Nette\Routing\Router $router, | ||
private Http\UrlScript $refUrl, | ||
private readonly Nette\Application\IPresenterFactory $presenterFactory, | ||
) { | ||
} | ||
|
||
|
||
/** | ||
* @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" | ||
* @param string $mode forward|redirect|link | ||
* @throws InvalidLinkException | ||
* @internal | ||
*/ | ||
public function generateUrl( | ||
Component $component, | ||
string $destination, | ||
array $args, | ||
string $mode, | ||
): ?string | ||
{ | ||
// note: createRequest supposes that saveState(), run() & tryCall() behaviour is final | ||
|
||
$this->lastCreatedRequest = $this->lastCreatedRequestFlag = null; | ||
|
||
$parts = static::parseDestination($destination); | ||
$path = $parts['path']; | ||
$args = $parts['args'] ?? $args; | ||
|
||
if (!$component instanceof Presenter || $parts['signal']) { | ||
[$cname, $signal] = Helpers::splitName($path); | ||
if ($cname !== '') { | ||
$component = $component->getComponent(strtr($cname, ':', '-')); | ||
} | ||
|
||
if ($signal === '') { | ||
throw new InvalidLinkException('Signal must be non-empty string.'); | ||
} | ||
|
||
$path = 'this'; | ||
} | ||
|
||
if ($path[0] === '@') { | ||
if (!$this->presenterFactory instanceof Application\PresenterFactory) { | ||
throw new Nette\InvalidStateException('Link aliasing requires PresenterFactory service.'); | ||
} | ||
$path = ':' . $this->presenterFactory->getAlias(substr($path, 1)); | ||
} | ||
|
||
$current = false; | ||
[$presenter, $action] = Helpers::splitName($path); | ||
if ($presenter === '') { | ||
$action = $path === 'this' ? $this->presenter->getAction() : $action; | ||
$presenter = $this->presenter->getName(); | ||
$presenterClass = $this->presenter::class; | ||
|
||
} else { | ||
if ($presenter[0] === ':') { // absolute | ||
$presenter = substr($presenter, 1); | ||
if (!$presenter) { | ||
throw new InvalidLinkException("Missing presenter name in '$destination'."); | ||
} | ||
} else { // relative | ||
[$module, , $sep] = Helpers::splitName($this->presenter->getName()); | ||
$presenter = $module . $sep . $presenter; | ||
} | ||
|
||
try { | ||
$presenterClass = $this->presenterFactory->getPresenterClass($presenter); | ||
} catch (Application\InvalidPresenterException $e) { | ||
throw new InvalidLinkException($e->getMessage(), 0, $e); | ||
} | ||
} | ||
|
||
// PROCESS SIGNAL ARGUMENTS | ||
if (isset($signal)) { // $component must be StatePersistent | ||
$reflection = new ComponentReflection($component::class); | ||
if ($signal === 'this') { // means "no signal" | ||
$signal = ''; | ||
if (array_key_exists(0, $args)) { | ||
throw new InvalidLinkException("Unable to pass parameters to 'this!' signal."); | ||
} | ||
} elseif (!str_contains($signal, Component::NameSeparator)) { | ||
// counterpart of signalReceived() & tryCall() | ||
|
||
$method = $reflection->getSignalMethod($signal); | ||
if (!$method) { | ||
throw new InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()"); | ||
} elseif ( | ||
$this->presenter->invalidLinkMode | ||
&& (ComponentReflection::parseAnnotation($method, 'deprecated') || $method->getAttributes(Attributes\Deprecated::class)) | ||
) { | ||
trigger_error("Link to deprecated signal '$signal'" . ($component === $this->presenter ? '' : ' in ' . $component::class) . " from '{$this->presenter->getName()}:{$this->presenter->getAction()}'.", E_USER_DEPRECATED); | ||
} | ||
|
||
// convert indexed parameters to named | ||
static::argsToParams($component::class, $method->getName(), $args, [], $missing); | ||
} | ||
|
||
// counterpart of StatePersistent | ||
if ($args && array_intersect_key($args, $reflection->getPersistentParams())) { | ||
$component->saveState($args); | ||
} | ||
|
||
if ($args && $component !== $this->presenter) { | ||
$prefix = $component->getUniqueId() . Component::NameSeparator; | ||
foreach ($args as $key => $val) { | ||
unset($args[$key]); | ||
$args[$prefix . $key] = $val; | ||
} | ||
} | ||
} | ||
|
||
// PROCESS ARGUMENTS | ||
if (is_subclass_of($presenterClass, Presenter::class)) { | ||
if ($action === '') { | ||
$action = Presenter::DefaultAction; | ||
} | ||
|
||
$current = ($action === '*' || strcasecmp($action, $this->presenter->getAction()) === 0) && $presenterClass === $this->presenter::class; | ||
|
||
$reflection = new ComponentReflection($presenterClass); | ||
if ($this->presenter->invalidLinkMode | ||
&& (ComponentReflection::parseAnnotation($reflection, 'deprecated') || $reflection->getAttributes(Attributes\Deprecated::class)) | ||
) { | ||
trigger_error("Link to deprecated presenter '$presenter' from '{$this->presenter->getName()}:{$this->presenter->getAction()}'.", E_USER_DEPRECATED); | ||
} | ||
|
||
// counterpart of run() & tryCall() | ||
if ($method = $reflection->getActionRenderMethod($action)) { | ||
if ( | ||
$this->presenter->invalidLinkMode | ||
&& (ComponentReflection::parseAnnotation($method, 'deprecated') || $method->getAttributes(Attributes\Deprecated::class)) | ||
) { | ||
trigger_error("Link to deprecated action '$presenter:$action' from '{$this->presenter->getName()}:{$this->presenter->getAction()}'.", E_USER_DEPRECATED); | ||
} | ||
|
||
static::argsToParams($presenterClass, $method->getName(), $args, $path === 'this' ? $this->presenter->getParameters() : [], $missing); | ||
|
||
} elseif (array_key_exists(0, $args)) { | ||
throw new InvalidLinkException("Unable to pass parameters to action '$presenter:$action', missing corresponding method."); | ||
} | ||
|
||
// counterpart of StatePersistent | ||
if (empty($signal) && $args && array_intersect_key($args, $reflection->getPersistentParams())) { | ||
$this->presenter->saveState($args, $reflection); | ||
} | ||
|
||
$globalState = $this->presenter->getGlobalState($path === 'this' ? null : $presenterClass); | ||
if ($current && $args) { | ||
$tmp = $globalState + $this->presenter->getParameters(); | ||
foreach ($args as $key => $val) { | ||
if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) { | ||
$current = false; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
$args += $globalState; | ||
} | ||
|
||
if ($mode !== 'test' && !empty($missing)) { | ||
foreach ($missing as $rp) { | ||
if (!array_key_exists($rp->getName(), $args)) { | ||
throw new InvalidLinkException("Missing parameter \${$rp->getName()} required by {$rp->getDeclaringClass()->getName()}::{$rp->getDeclaringFunction()->getName()}()"); | ||
} | ||
} | ||
} | ||
|
||
// ADD ACTION & SIGNAL & FLASH | ||
if ($action) { | ||
$args[Presenter::ActionKey] = $action; | ||
} | ||
|
||
if (!empty($signal)) { | ||
$args[Presenter::SignalKey] = $component->getParameterId($signal); | ||
$current = $current && $args[Presenter::SignalKey] === $this->presenter->getParameter(Presenter::SignalKey); | ||
} | ||
|
||
if (($mode === 'redirect' || $mode === 'forward') && $this->presenter->hasFlashSession()) { | ||
$flashKey = $this->presenter->getParameter(Presenter::FlashKey); | ||
$args[Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ?: null; | ||
} | ||
|
||
$this->lastCreatedRequest = new Application\Request($presenter, Application\Request::FORWARD, $args); | ||
$this->lastCreatedRequestFlag = ['current' => $current]; | ||
|
||
return $mode === 'forward' || $mode === 'test' | ||
? null | ||
: $this->requestToUrl($this->lastCreatedRequest, $mode === 'link' && !$parts['absolute'] && !$this->presenter->absoluteUrls) . $parts['fragment']; | ||
} | ||
|
||
|
||
/** | ||
* Parse destination in format "[//] [[[module:]presenter:]action | signal! | this] [?query] [#fragment]" | ||
* @throws InvalidLinkException | ||
* @internal | ||
*/ | ||
public static function parseDestination(string $destination): array | ||
{ | ||
if (!preg_match('~^ (?<absolute>//)?+ (?<path>[^!?#]++) (?<signal>!)?+ (?<query>\?[^#]*)?+ (?<fragment>\#.*)?+ $~x', $destination, $matches)) { | ||
throw new InvalidLinkException("Invalid destination '$destination'."); | ||
} | ||
|
||
if (!empty($matches['query'])) { | ||
parse_str(substr($matches['query'], 1), $args); | ||
} | ||
|
||
return [ | ||
'absolute' => (bool) $matches['absolute'], | ||
'path' => $matches['path'], | ||
'signal' => !empty($matches['signal']), | ||
'args' => $args ?? null, | ||
'fragment' => $matches['fragment'] ?? '', | ||
]; | ||
} | ||
|
||
|
||
/** | ||
* Converts list of arguments to named parameters & check types. | ||
* @param \ReflectionParameter[] $missing arguments | ||
* @throws InvalidLinkException | ||
* @internal | ||
*/ | ||
public static function argsToParams( | ||
string $class, | ||
string $method, | ||
array &$args, | ||
array $supplemental = [], | ||
?array &$missing = null, | ||
): void | ||
{ | ||
$i = 0; | ||
$rm = new \ReflectionMethod($class, $method); | ||
foreach ($rm->getParameters() as $param) { | ||
$type = ComponentReflection::getType($param); | ||
$name = $param->getName(); | ||
|
||
if (array_key_exists($i, $args)) { | ||
$args[$name] = $args[$i]; | ||
unset($args[$i]); | ||
$i++; | ||
|
||
} elseif (array_key_exists($name, $args)) { | ||
// continue with process | ||
|
||
} elseif (array_key_exists($name, $supplemental)) { | ||
$args[$name] = $supplemental[$name]; | ||
} | ||
|
||
if (!isset($args[$name])) { | ||
if ( | ||
!$param->isDefaultValueAvailable() | ||
&& !$param->allowsNull() | ||
&& $type !== 'scalar' | ||
&& $type !== 'array' | ||
&& $type !== 'iterable' | ||
) { | ||
$missing[] = $param; | ||
unset($args[$name]); | ||
} | ||
|
||
continue; | ||
} | ||
|
||
if (!ComponentReflection::convertType($args[$name], $type)) { | ||
throw new InvalidLinkException(sprintf( | ||
'Argument $%s passed to %s() must be %s, %s given.', | ||
$name, | ||
$rm->getDeclaringClass()->getName() . '::' . $rm->getName(), | ||
$type, | ||
get_debug_type($args[$name]), | ||
)); | ||
} | ||
|
||
$def = $param->isDefaultValueAvailable() | ||
? $param->getDefaultValue() | ||
: null; | ||
if ($args[$name] === $def || ($def === null && $args[$name] === '')) { | ||
$args[$name] = null; // value transmit is unnecessary | ||
} | ||
} | ||
|
||
if (array_key_exists($i, $args)) { | ||
throw new InvalidLinkException("Passed more parameters than method $class::{$rm->getName()}() expects."); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Converts Request to URL. | ||
*/ | ||
public function requestToUrl(Application\Request $request, ?bool $relative = true): string | ||
{ | ||
$url = $this->router->constructUrl($request->toArray(), $this->refUrl); | ||
if ($url === null) { | ||
$params = $request->getParameters(); | ||
unset($params[Presenter::ActionKey], $params[Presenter::PresenterKey]); | ||
$params = urldecode(http_build_query($params, '', ', ')); | ||
throw new InvalidLinkException("No route for {$request->getPresenterName()}:{$request->getParameter('action')}($params)"); | ||
} | ||
|
||
if ($relative) { | ||
$hostUrl = $this->refUrl->getHostUrl() . '/'; | ||
if (strncmp($url, $hostUrl, strlen($hostUrl)) === 0) { | ||
$url = substr($url, strlen($hostUrl) - 1); | ||
} | ||
} | ||
|
||
return $url; | ||
} | ||
} |
Oops, something went wrong.