Skip to content
Closed
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This extension provides following features:
* Provides correct return types for `TreeBuilder` and `NodeDefinition` objects.
* Notifies you when you try to get an unregistered service from the container.
* Notifies you when you try to get a private service from the container.
* Notifies you when you try to generate a URL for a non-existing route name.
* Optionally correct return types for `InputInterface::getArgument()`, `::getOption`, `::hasArgument`, and `::hasOption`.


Expand Down Expand Up @@ -148,3 +149,13 @@ Call the new env in your `console-application.php`:
```php
$kernel = new \App\Kernel('phpstan_env', (bool) $_SERVER['APP_DEBUG']);
```

# Analysis of generating URLs

You have to provide a path to `url_generating_routes.php` for the url generating analysis to work.

```yaml
parameters:
symfony:
urlGeneratingRulesFile: var/cache/dev/url_generating_routes.xml
```
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"symfony/http-foundation": "^5.1",
"symfony/messenger": "^4.2 || ^5.0",
"symfony/polyfill-php80": "^1.24",
"symfony/routing": "^4.4 || ^5.0",
"symfony/serializer": "^4.0 || ^5.0"
},
"config": {
Expand Down
11 changes: 11 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ parameters:
constantHassers: true
console_application_loader: null
consoleApplicationLoader: null
url_generating_rules_file: null
urlGeneratingRulesFile: null
stubFiles:
- stubs/Psr/Cache/CacheItemInterface.stub
- stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub
Expand Down Expand Up @@ -65,6 +67,8 @@ parametersSchema:
constantHassers: bool()
console_application_loader: schema(string(), nullable())
consoleApplicationLoader: schema(string(), nullable())
url_generating_rules_file: schema(string(), nullable())
urlGeneratingRulesFile: schema(string(), nullable())
])

services:
Expand All @@ -89,6 +93,13 @@ services:
-
factory: @symfony.parameterMapFactory::create()

# url generating routes map
symfony.urlGeneratingRoutesMapFactory:
class: PHPStan\Symfony\UrlGeneratingRoutesMapFactory
factory: PHPStan\Symfony\PhpUrlGeneratingRoutesMapFactory
-
factory: @symfony.urlGeneratingRoutesMapFactory::create()

# ControllerTrait::get()/has() return type
-
factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface)
Expand Down
1 change: 1 addition & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ rules:
- PHPStan\Rules\Symfony\InvalidArgumentDefaultValueRule
- PHPStan\Rules\Symfony\UndefinedOptionRule
- PHPStan\Rules\Symfony\InvalidOptionDefaultValueRule
- PHPStan\Rules\Symfony\UrlGeneratorInterfaceUnknownRouteRule

75 changes: 75 additions & 0 deletions src/Rules/Symfony/UrlGeneratorInterfaceUnknownRouteRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Symfony;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;
use PHPStan\Symfony\UrlGeneratingRoutesMap;
use PHPStan\Type\ObjectType;

/**
* @implements Rule<MethodCall>
*/
final class UrlGeneratorInterfaceUnknownRouteRule implements Rule
{

/** @var UrlGeneratingRoutesMap */
private $urlGeneratingRoutesMap;

public function __construct(UrlGeneratingRoutesMap $urlGeneratingRoutesMap)
{
$this->urlGeneratingRoutesMap = $urlGeneratingRoutesMap;
}

public function getNodeType(): string
{
return MethodCall::class;
}

/**
* @param \PhpParser\Node $node
* @param \PHPStan\Analyser\Scope $scope
* @return (string|\PHPStan\Rules\RuleError)[] errors
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node instanceof MethodCall) {
throw new ShouldNotHappenException();
}

if (!$node->name instanceof Node\Identifier) {
return [];
}

if (in_array($node->name->name, ['generate', 'generateUrl'], true) === false || !isset($node->getArgs()[0])) {
return [];
}

$argType = $scope->getType($node->var);
$isControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\Controller'))->isSuperTypeOf($argType);
$isAbstractControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\AbstractController'))->isSuperTypeOf($argType);
$isUrlGeneratorInterface = (new ObjectType('Symfony\Component\Routing\Generator\UrlGeneratorInterface'))->isSuperTypeOf($argType);
if (
$isControllerType->no()
&& $isAbstractControllerType->no()
&& $isUrlGeneratorInterface->no()
) {
return [];
}

$routeName = $this->urlGeneratingRoutesMap::getRouteNameFromNode($node->getArgs()[0]->value, $scope);
if ($routeName === null) {
return [];
}

if ($this->urlGeneratingRoutesMap->hasRouteName($routeName) === false) {
return [sprintf('Route with name "%s" does not exist.', $routeName)];
}

return [];
}

}
5 changes: 5 additions & 0 deletions src/Symfony/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ public function getConsoleApplicationLoader(): ?string
return $this->parameters['consoleApplicationLoader'] ?? $this->parameters['console_application_loader'] ?? null;
}

public function getUrlGeneratingRoutesFile(): ?string
{
return $this->parameters['urlGeneratingRulesFile'] ?? $this->parameters['url_generating_rules_file'] ?? null;
}

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

namespace PHPStan\Symfony;

use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
use PHPStan\Type\TypeUtils;
use function count;

final class DefaultUrlGeneratingRoutesMap implements UrlGeneratingRoutesMap
{

/** @var \PHPStan\Symfony\UrlGeneratingRoutesDefinition[] */
private $routes;

/**
* @param \PHPStan\Symfony\UrlGeneratingRoutesDefinition[] $routes
*/
public function __construct(array $routes)
{
$this->routes = $routes;
}

public function hasRouteName(string $name): bool
{
foreach ($this->routes as $route) {
if ($route->getName() === $name) {
return true;
}
}

return false;
}

public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string
{
$strings = TypeUtils::getConstantStrings($scope->getType($node));
return count($strings) === 1 ? $strings[0]->getValue() : null;
}

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

namespace PHPStan\Symfony;

use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;

final class FakeUrlGeneratingRoutesMap implements UrlGeneratingRoutesMap
{

public function hasRouteName(string $name): bool
{
return false;
}

public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string
{
return null;
}

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

namespace PHPStan\Symfony;

use function sprintf;

final class PhpUrlGeneratingRoutesMapFactory implements UrlGeneratingRoutesMapFactory
{

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

public function __construct(Configuration $configuration)
{
$this->urlGeneratingRoutesFile = $configuration->getUrlGeneratingRoutesFile();
}

public function create(): UrlGeneratingRoutesMap
{
if ($this->urlGeneratingRoutesFile === null) {
return new FakeUrlGeneratingRoutesMap();
}

if (file_exists($this->urlGeneratingRoutesFile) === false) {
throw new UrlGeneratingRoutesFileNotExistsException(sprintf('File %s containing route generator information does not exist.', $this->urlGeneratingRoutesFile));
}

$urlGeneratingRoutes = require $this->urlGeneratingRoutesFile;

if (!is_array($urlGeneratingRoutes)) {
throw new UrlGeneratingRoutesFileNotExistsException(sprintf('File %s containing route generator information cannot be parsed.', $this->urlGeneratingRoutesFile));
}

/** @var \PHPStan\Symfony\UrlGeneratingRoutesDefinition[] $routes */
$routes = [];
foreach ($urlGeneratingRoutes as $routeName => $routeConfiguration) {
if (!is_string($routeName)) {
continue;
}

if (!is_array($routeConfiguration) || !isset($routeConfiguration[1]['_controller'])) {
continue;
}

$routes[] = new UrlGeneratingRoute($routeName, $routeConfiguration[1]['_controller']);
}

return new DefaultUrlGeneratingRoutesMap($routes);
}

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

namespace PHPStan\Symfony;

class UrlGeneratingRoute implements UrlGeneratingRoutesDefinition
{

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

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

public function __construct(
string $name,
string $controller
)
{
$this->name = $name;
$this->controller = $controller;
}

public function getName(): string
{
return $this->name;
}

public function getController(): ?string
{
return $this->controller;
}

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

namespace PHPStan\Symfony;

interface UrlGeneratingRoutesDefinition
{

public function getName(): string;

public function getController(): ?string;

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

namespace PHPStan\Symfony;

final class UrlGeneratingRoutesFileNotExistsException extends \InvalidArgumentException
{

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

namespace PHPStan\Symfony;

use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;

interface UrlGeneratingRoutesMap
{

public function hasRouteName(string $name): bool;

public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string;

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

namespace PHPStan\Symfony;

interface UrlGeneratingRoutesMapFactory
{

public function create(): UrlGeneratingRoutesMap;

}
20 changes: 20 additions & 0 deletions tests/Rules/Symfony/ExampleControllerWithRouting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Symfony;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

final class ExampleControllerWithRouting extends AbstractController
{

public function generateSomeRoute1(): void
{
$this->generateUrl('someRoute1');
}

public function generateNonExistingRoute(): void
{
$this->generateUrl('unknown');
}

}
Loading