Skip to content

Commit fe3366f

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

38 files changed

+1718
-0
lines changed

README.md

+64
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ It also contains these framework-specific rules (can be enabled separately):
2424
* Do not extend Nette\Object, use Nette\SmartObject trait instead
2525
* Rethrow exceptions that are always meant to be rethrown (like `AbortException`)
2626

27+
Links checking (can be enabled separately by - see configuration):
28+
* Validate parameters passed to 'link()', 'lazyLink()', 'redirect()', 'redirectPermanent()', 'forward()', 'isLinkCurrent()' and 'canonicalize()' methods
29+
* Works for presenters, components and 'LinkGenerator' service
30+
* Checks if passed destination is valid and points to existing presenter, action or signal
31+
* Checks if passed link parameters are valid and match relevant 'action*()', 'render*()' or 'handle*()' method signature
32+
* Checks also links to sub-components of known types (createComponent*() method must exists)
2733

2834
## Installation
2935

@@ -52,3 +58,61 @@ To perform framework-specific checks, include also this file:
5258
```
5359

5460
</details>
61+
62+
## Configuration
63+
64+
### containerLoader
65+
66+
Container loader can be used to create instance of Nette application DI container.
67+
68+
Example:
69+
```neon
70+
parameters:
71+
nette:
72+
containerLoader: './containerLoader.php'
73+
```
74+
75+
Example `containerLoader.php`:
76+
77+
```php
78+
<?php
79+
80+
return App\Bootstrap::boot()->createContainer();
81+
```
82+
83+
### applicationMapping
84+
85+
Application mapping is used to map presenter identfiers to classes in link checking.
86+
87+
Example:
88+
```neon
89+
parameters:
90+
nette:
91+
applicationMapping:
92+
*: App\Presenters\*\*Presenter
93+
```
94+
95+
### checkLinks
96+
97+
Link checking can be disabled/enabled by setting `checkLinks` parameter. It is enabled by default if `bleedingEndge` is enabled.
98+
99+
Either `applicationMapping` or `containerLoader` (for automatically loading mappings from `PresenterFactory` service in your app) must be set for link checking to work.
100+
101+
Example:
102+
```neon
103+
parameters:
104+
nette:
105+
checkLinks: true
106+
```
107+
108+
If you use non-standard `PresenterFactory` this feature might not work because logic for mapping presenter name (e.g. `MyModule:Homepage`) to presenter class (e.g. `\App\Presenters\MyModule\HomepagePresenter`) and vice versa would work differently.
109+
110+
If you use `containerLoader` you might solve this by implementing method `unformatPresenterClass` in your custom `PresenterFactory` class. This method should return presenter name for given presenter class.
111+
112+
Or you can create custom implementation overriding `PHPStan\Nette\PresenterResolver` service and replace it in your PHPStan config:
113+
114+
```neon
115+
services:
116+
nettePresenterResolver:
117+
class: MyCustom\PresenterResolver
118+
```

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

+27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
parameters:
2+
nette:
3+
containerLoader: null
4+
applicationMapping: []
5+
checkLinks: %featureToggles.bleedingEdge%
26
additionalConstructors:
37
- Nette\Application\UI\Presenter::startup
48
exceptions:
@@ -48,7 +52,30 @@ parameters:
4852
- terminate
4953
- forward
5054

55+
parametersSchema:
56+
nette: structure([
57+
containerLoader: schema(string(), nullable())
58+
applicationMapping: arrayOf(anyOf(string(), listOf(string())))
59+
checkLinks: bool()
60+
])
61+
5162
services:
63+
netteContainerResolver:
64+
class: PHPStan\Nette\ContainerResolver
65+
arguments:
66+
- %nette.containerLoader%
67+
68+
nettePresenterResolver:
69+
class: PHPStan\Nette\PresenterResolver
70+
arguments:
71+
- %nette.applicationMapping%
72+
73+
-
74+
class: PHPStan\Nette\LinkChecker
75+
76+
-
77+
class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension
78+
5279
-
5380
class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension
5481
tags:

rules.neon

+12
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ rules:
2020
conditionalTags:
2121
PHPStan\Rule\Nette\RegularExpressionPatternRule:
2222
phpstan.rules.rule: %featureToggles.bleedingEdge%
23+
PHPStan\Rule\Nette\ComponentLinksRule:
24+
phpstan.rules.rule: %nette.checkLinks%
25+
PHPStan\Rule\Nette\PresenterLinksRule:
26+
phpstan.rules.rule: %nette.checkLinks%
27+
PHPStan\Rule\Nette\LinkGeneratorLinksRule:
28+
phpstan.rules.rule: %nette.checkLinks%
2329

2430
services:
2531
-
@@ -30,3 +36,9 @@ services:
3036
- phpstan.rules.rule
3137
-
3238
class: PHPStan\Rule\Nette\RegularExpressionPatternRule
39+
-
40+
class: PHPStan\Rule\Nette\ComponentLinksRule
41+
-
42+
class: PHPStan\Rule\Nette\PresenterLinksRule
43+
-
44+
class: PHPStan\Rule\Nette\LinkGeneratorLinksRule
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+
}
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 PresenterResolvingException 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 PresenterResolvingNotAvailableException extends PresenterResolvingException
6+
{
7+
8+
}

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+
}

0 commit comments

Comments
 (0)