Skip to content

Commit

Permalink
LinksRule for checking validity of links
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinMystikJonas committed Aug 16, 2023
1 parent 6ec9016 commit e01ed37
Show file tree
Hide file tree
Showing 38 changed files with 1,718 additions and 0 deletions.
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ It also contains these framework-specific rules (can be enabled separately):
* Do not extend Nette\Object, use Nette\SmartObject trait instead
* Rethrow exceptions that are always meant to be rethrown (like `AbortException`)

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

## Installation

Expand Down Expand Up @@ -52,3 +58,61 @@ To perform framework-specific checks, include also this file:
```

</details>

## Configuration

### containerLoader

Container loader can be used to create instance of Nette application DI container.

Example:
```neon
parameters:
nette:
containerLoader: './containerLoader.php'
```

Example `containerLoader.php`:

```php
<?php

return App\Bootstrap::boot()->createContainer();
```

### applicationMapping

Application mapping is used to map presenter identfiers to classes in link checking.

Example:
```neon
parameters:
nette:
applicationMapping:
*: App\Presenters\*\*Presenter
```

### checkLinks

Link checking can be disabled/enabled by setting `checkLinks` parameter. It is enabled by default if `bleedingEndge` is enabled.

Either `applicationMapping` or `containerLoader` (for automatically loading mappings from `PresenterFactory` service in your app) must be set for link checking to work.

Example:
```neon
parameters:
nette:
checkLinks: true
```

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.

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.

Or you can create custom implementation overriding `PHPStan\Nette\PresenterResolver` service and replace it in your PHPStan config:

```neon
services:
nettePresenterResolver:
class: MyCustom\PresenterResolver
```
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"require-dev": {
"nette/application": "^3.0",
"nette/di": "^2.3.0 || ^3.0.0",
"nette/forms": "^3.0",
"nette/utils": "^2.3.0 || ^3.0.0",
"nikic/php-parser": "^4.13.2",
Expand Down
27 changes: 27 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
parameters:
nette:
containerLoader: null
applicationMapping: []
checkLinks: %featureToggles.bleedingEdge%
additionalConstructors:
- Nette\Application\UI\Presenter::startup
exceptions:
Expand Down Expand Up @@ -48,7 +52,30 @@ parameters:
- terminate
- forward

parametersSchema:
nette: structure([
containerLoader: schema(string(), nullable())
applicationMapping: arrayOf(string(), anyOf(string(), arrayOf(string())))
checkLinks: bool()
])

services:
netteContainerResolver:
class: PHPStan\Nette\ContainerResolver
arguments:
- %nette.containerLoader%

nettePresenterResolver:
class: PHPStan\Nette\PresenterResolver
arguments:
- %nette.applicationMapping%

-
class: PHPStan\Nette\LinkChecker

-
class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension

-
class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension
tags:
Expand Down
12 changes: 12 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ rules:
conditionalTags:
PHPStan\Rule\Nette\RegularExpressionPatternRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%
PHPStan\Rule\Nette\ComponentLinksRule:
phpstan.rules.rule: %nette.checkLinks%
PHPStan\Rule\Nette\PresenterLinksRule:
phpstan.rules.rule: %nette.checkLinks%
PHPStan\Rule\Nette\LinkGeneratorLinksRule:
phpstan.rules.rule: %nette.checkLinks%

services:
-
Expand All @@ -30,3 +36,9 @@ services:
- phpstan.rules.rule
-
class: PHPStan\Rule\Nette\RegularExpressionPatternRule
-
class: PHPStan\Rule\Nette\ComponentLinksRule
-
class: PHPStan\Rule\Nette\PresenterLinksRule
-
class: PHPStan\Rule\Nette\LinkGeneratorLinksRule
25 changes: 25 additions & 0 deletions src/Exceptions/InvalidLinkDestinationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

use Throwable;
use function sprintf;

class InvalidLinkDestinationException extends InvalidLinkException
{

/** @var string */
private $destination;

public function __construct(string $destination, int $code = 0, ?Throwable $previous = null)
{
parent::__construct(sprintf("Invalid link destination '%s'", $destination), $code, $previous);
$this->destination = $destination;
}

public function getDestination(): string
{
return $this->destination;
}

}
10 changes: 10 additions & 0 deletions src/Exceptions/InvalidLinkException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

use RuntimeException;

class InvalidLinkException extends RuntimeException
{

}
8 changes: 8 additions & 0 deletions src/Exceptions/InvalidLinkParamsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

class InvalidLinkParamsException extends InvalidLinkException
{

}
10 changes: 10 additions & 0 deletions src/Exceptions/LinkCheckFailedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

use RuntimeException;

class LinkCheckFailedException extends RuntimeException
{

}
10 changes: 10 additions & 0 deletions src/Exceptions/PresenterResolvingException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

use RuntimeException;

class PresenterResolvingException extends RuntimeException
{

}
8 changes: 8 additions & 0 deletions src/Exceptions/PresenterResolvingNotAvailableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

class PresenterResolvingNotAvailableException extends PresenterResolvingException
{

}
66 changes: 66 additions & 0 deletions src/Nette/ContainerResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php declare(strict_types = 1);

namespace PHPStan\Nette;

use Nette\DI\Container;
use PHPStan\ShouldNotHappenException;
use function is_file;
use function is_readable;
use function sprintf;

class ContainerResolver
{

/** @var string|null */
private $containerLoader;

/** @var Container|false|null */
private $container;

public function __construct(?string $containerLoader)
{
$this->containerLoader = $containerLoader;
}

public function getContainer(): ?Container
{
if ($this->container === false) {
return null;
}

if ($this->container !== null) {
return $this->container;
}

if ($this->containerLoader === null) {
$this->container = false;

return null;
}

$this->container = $this->loadContainer($this->containerLoader);

return $this->container;
}


private function loadContainer(string $containerLoader): ?Container
{
if (!is_file($containerLoader)) {
throw new ShouldNotHappenException(sprintf(
'Nette container could not be loaded: file "%s" does not exist',
$containerLoader
));
}

if (!is_readable($containerLoader)) {
throw new ShouldNotHappenException(sprintf(
'Nette container could not be loaded: file "%s" is not readable',
$containerLoader
));
}

return require $containerLoader;
}

}
Loading

0 comments on commit e01ed37

Please sign in to comment.