Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LinksRule for checking validity of links #132

Open
wants to merge 1 commit into
base: 1.1.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(anyOf(string(), listOf(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