Skip to content

Commit 428326c

Browse files
LinksRule for checking validity of links
1 parent 6ec9016 commit 428326c

26 files changed

+1207
-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:

rules.neon

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ parametersSchema:
1616

1717
rules:
1818
- PHPStan\Rule\Nette\DoNotExtendNetteObjectRule
19+
- PHPStan\Rule\Nette\LinksRule
1920

2021
conditionalTags:
2122
PHPStan\Rule\Nette\RegularExpressionPatternRule:
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

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

0 commit comments

Comments
 (0)