Skip to content

Commit 5378e63

Browse files
LinksRule for checking validity of links
1 parent 6ec9016 commit 5378e63

25 files changed

+1197
-0
lines changed

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
"require-dev": {
2121
"nette/application": "^3.0",
22+
"nette/di": "^2.3.0 || ^3.0.0",
2223
"nette/forms": "^3.0",
2324
"nette/utils": "^2.3.0 || ^3.0.0",
2425
"nikic/php-parser": "^4.13.2",

extension.neon

+19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
parameters:
2+
nette:
3+
containerLoader: null
4+
applicationMapping: []
25
additionalConstructors:
36
- Nette\Application\UI\Presenter::startup
47
exceptions:
@@ -48,7 +51,23 @@ parameters:
4851
- terminate
4952
- forward
5053

54+
parametersSchema:
55+
nette: structure([
56+
containerLoader: schema(string(), nullable())
57+
applicationMapping: arrayOf(string(), string())
58+
])
59+
5160
services:
61+
netteContainerResolver:
62+
class: PHPStan\Nette\ContainerResolver
63+
arguments:
64+
- %nette.containerLoader%
65+
66+
nettePresenterResolver:
67+
class: PHPStan\Nette\PresenterResolver
68+
arguments:
69+
- %nette.applicationMapping%
70+
5271
-
5372
class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension
5473
tags:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Exceptions;
4+
5+
use Throwable;
6+
use function sprintf;
7+
8+
class InvalidLinkDestinationException extends InvalidLinkException
9+
{
10+
11+
/** @var string */
12+
private $destination;
13+
14+
public function __construct(string $destination, int $code = 0, ?Throwable $previous = null)
15+
{
16+
parent::__construct(sprintf("Invalid link destination '%s'", $destination), $code, $previous);
17+
$this->destination = $destination;
18+
}
19+
20+
public function getDestination(): string
21+
{
22+
return $this->destination;
23+
}
24+
25+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Exceptions;
4+
5+
use RuntimeException;
6+
7+
class InvalidLinkException extends RuntimeException
8+
{
9+
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Exceptions;
4+
5+
class InvalidLinkParamsException extends InvalidLinkException
6+
{
7+
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Exceptions;
4+
5+
use RuntimeException;
6+
7+
class LinkCheckFailedException extends RuntimeException
8+
{
9+
10+
}

src/Nette/ContainerResolver.php

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Nette;
4+
5+
use Nette\DI\Container;
6+
use PHPStan\ShouldNotHappenException;
7+
use function is_file;
8+
use function is_readable;
9+
use function sprintf;
10+
11+
class ContainerResolver
12+
{
13+
14+
/** @var string|null */
15+
private $containerLoader;
16+
17+
/** @var Container|false|null */
18+
private $container;
19+
20+
public function __construct(?string $containerLoader)
21+
{
22+
$this->containerLoader = $containerLoader;
23+
}
24+
25+
public function getContainer(): ?Container
26+
{
27+
if ($this->container === false) {
28+
return null;
29+
}
30+
31+
if ($this->container !== null) {
32+
return $this->container;
33+
}
34+
35+
if ($this->containerLoader === null) {
36+
$this->container = false;
37+
38+
return null;
39+
}
40+
41+
$this->container = $this->loadContainer($this->containerLoader);
42+
43+
return $this->container;
44+
}
45+
46+
47+
private function loadContainer(string $containerLoader): ?Container
48+
{
49+
if (!is_file($containerLoader)) {
50+
throw new ShouldNotHappenException(sprintf(
51+
'Nette container could not be loaded: file "%s" does not exist',
52+
$containerLoader
53+
));
54+
}
55+
56+
if (!is_readable($containerLoader)) {
57+
throw new ShouldNotHappenException(sprintf(
58+
'Nette container could not be loaded: file "%s" is not readable',
59+
$containerLoader
60+
));
61+
}
62+
63+
return require $containerLoader;
64+
}
65+
66+
}

src/Nette/PresenterResolver.php

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Nette;
4+
5+
use Nette\Application\IPresenterFactory;
6+
use Nette\Application\PresenterFactory;
7+
use PHPStan\Exceptions\LinkCheckFailedException;
8+
use PHPStan\ShouldNotHappenException;
9+
use ReflectionClass;
10+
use ReflectionException;
11+
use function count;
12+
use function is_array;
13+
use function is_string;
14+
use function preg_match;
15+
use function preg_replace;
16+
use function sprintf;
17+
use function str_replace;
18+
use function strrpos;
19+
use function substr;
20+
21+
class PresenterResolver
22+
{
23+
24+
/** @var array<string, string|array{0: string, 1: string, 2: string}> */
25+
protected $mapping;
26+
27+
/** @var ContainerResolver */
28+
private $containerResolver;
29+
30+
/** @var IPresenterFactory */
31+
private $presenterFactory;
32+
33+
/**
34+
* @param array<string, string|array{0: string, 1: string, 2: string}> $mapping
35+
*/
36+
public function __construct(array $mapping, ContainerResolver $containerResolver)
37+
{
38+
$this->mapping = $mapping;
39+
$this->containerResolver = $containerResolver;
40+
}
41+
42+
protected function getPresenterFactory(): IPresenterFactory
43+
{
44+
if ($this->presenterFactory === null) {
45+
if ($this->containerResolver->getContainer() !== null) {
46+
$this->presenterFactory = $this->containerResolver->getContainer()->getByType(IPresenterFactory::class);
47+
} else {
48+
$this->presenterFactory = new PresenterFactory();
49+
$this->presenterFactory->setMapping($this->mapping);
50+
}
51+
}
52+
return $this->presenterFactory;
53+
}
54+
55+
/**
56+
* @return array<string, array{0: string, 1: string, 2: string}>
57+
* @throws ShouldNotHappenException
58+
* @throws ReflectionException
59+
*/
60+
protected function getCurrentMapping(): array
61+
{
62+
if ($this->mapping !== []) {
63+
$convertedMapping = [];
64+
foreach ($this->mapping as $module => $mask) {
65+
if (is_string($mask)) {
66+
if (preg_match('#^\\\\?([\w\\\\]*\\\\)?(\w*\*\w*?\\\\)?([\w\\\\]*\*\w*)$#D', $mask, $m) !== 1) {
67+
throw new ShouldNotHappenException(sprintf("Invalid mapping mask '%s' in parameters.nette.applicationMapping.", $mask));
68+
}
69+
$convertedMapping[$module] = [$m[1], $m[2] !== '' ? $m[2] : '*Module\\', $m[3]];
70+
} elseif (is_array($mask) && count($mask) === 3) { /** @phpstan-ignore-line */
71+
$convertedMapping[$module] = [$mask[0] !== '' ? $mask[0] . '\\' : '', $mask[1] . '\\', $mask[2]];
72+
} else {
73+
throw new ShouldNotHappenException(sprintf('Invalid mapping mask for module %s in parameters.nette.applicationMapping.', $module));
74+
}
75+
}
76+
return $convertedMapping;
77+
}
78+
79+
$presenterFactory = $this->getPresenterFactory();
80+
if (!$presenterFactory instanceof PresenterFactory) {
81+
throw new ShouldNotHappenException(
82+
'PresenterFactory in your container is not instance of Nette\Application\PresenterFactory. We cannot get mapping from it.' .
83+
' Either set your mappings explicitly in parameters.nette.applicationMapping ' .
84+
' or replace service nettePresenterResolver with your own override of getCurrentMapping() or unformatPresenterClass().'
85+
);
86+
}
87+
88+
$mappingPropertyReflection = (new ReflectionClass($presenterFactory))->getProperty('mapping');
89+
$mappingPropertyReflection->setAccessible(true);
90+
/** @var array<string, array{0: string, 1: string, 2: string}> $mapping */
91+
$mapping = $mappingPropertyReflection->getValue($presenterFactory);
92+
93+
return $mapping;
94+
}
95+
96+
public function getPresenterClassByName(string $name, ?string $currentPresenterClass = null): string
97+
{
98+
$name = $this->resolvePresenterName($name, $currentPresenterClass);
99+
return $this->getPresenterFactory()->getPresenterClass($name);
100+
}
101+
102+
public function resolvePresenterName(string $name, ?string $currentPresenterClass = null): string
103+
{
104+
if ($name[0] === ':') {
105+
return substr($name, 1);
106+
}
107+
108+
if ($currentPresenterClass === null) {
109+
throw new LinkCheckFailedException(sprintf("Cannot resolve relative presenter name '%s' - current presenter is not set.", $name));
110+
}
111+
112+
$currentName = $this->unformatPresenterClass($currentPresenterClass);
113+
$currentNameSepPos = strrpos($currentName, ':');
114+
if ($currentNameSepPos !== false && $currentNameSepPos !== 0) {
115+
$currentModule = substr($currentName, 0, $currentNameSepPos);
116+
$currentPresenter = substr($currentName, $currentNameSepPos + 1);
117+
} else {
118+
$currentModule = '';
119+
$currentPresenter = $currentName;
120+
}
121+
122+
if ($name === 'this') {
123+
return $currentModule . ':' . $currentPresenter;
124+
}
125+
126+
return $currentModule . ':' . $name;
127+
}
128+
129+
protected function unformatPresenterClass(string $class): string
130+
{
131+
foreach ($this->getCurrentMapping() as $module => $mapping) {
132+
$mapping = str_replace(['\\', '*'], ['\\\\', '(\w+)'], $mapping);
133+
if (preg_match('#^\\\\?' . $mapping[0] . '((?:' . $mapping[1] . ')*)' . $mapping[2] . '$#Di', $class, $matches) === 1) {
134+
return ($module === '*' ? '' : $module . ':')
135+
. preg_replace('#' . $mapping[1] . '#iA', '$1:', $matches[1]) . $matches[3];
136+
}
137+
}
138+
139+
throw new LinkCheckFailedException(sprintf("Cannot convert presenter class '%s' to presenter name. No matching mapping found.", $class));
140+
}
141+
142+
}

0 commit comments

Comments
 (0)