Skip to content

Commit

Permalink
link generation moved to a new class UI\LinkBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Apr 16, 2024
1 parent f11699f commit 599cbe5
Show file tree
Hide file tree
Showing 8 changed files with 386 additions and 316 deletions.
2 changes: 1 addition & 1 deletion src/Application/LinkGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function link(string $dest, array $params = []): string
}

if ($method = $class::getReflection()->getActionRenderMethod($action)) {
UI\Presenter::argsToParams($class, $method->getName(), $params, [], $missing);
UI\LinkBuilder::argsToParams($class, $method->getName(), $params, [], $missing);
if ($missing) {
$rp = $missing[0];
throw new UI\InvalidLinkException("Missing parameter \${$rp->getName()} required by $class::{$method->getName()}()");
Expand Down
8 changes: 4 additions & 4 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()->createRequest($this, $destination, $args, 'link');
return $this->getPresenter()->getLinkBuilder()->generateUrl($this, $destination, $args, 'link');

} catch (InvalidLinkException $e) {
return $this->getPresenter()->handleInvalidLink($e);
Expand Down Expand Up @@ -284,7 +284,7 @@ public function isLinkCurrent(?string $destination = null, $args = []): bool
$args = func_num_args() < 3 && is_array($args)
? $args
: array_slice(func_get_args(), 1);
$this->getPresenter()->createRequest($this, $destination, $args, 'test');
$this->getPresenter()->getLinkBuilder()->generateUrl($this, $destination, $args, 'test');
}

return $this->getPresenter()->getLastCreatedRequestFlag('current');
Expand All @@ -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->createRequest($this, $destination, $args, 'redirect'));
$presenter->redirectUrl($presenter->getLinkBuilder()->generateUrl($this, $destination, $args, '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->createRequest($this, $destination, $args, 'redirect'),
$presenter->getLinkBuilder()->generateUrl($this, $destination, $args, 'redirect'),
Nette\Http\IResponse::S301_MovedPermanently,
);
}
Expand Down
339 changes: 339 additions & 0 deletions src/Application/UI/LinkBuilder.php
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;
}
}
Loading

0 comments on commit 599cbe5

Please sign in to comment.