├── .editorconfig ├── LICENSE ├── README.md ├── composer.json ├── extension.neon ├── phpcs.xml.dist └── src ├── Exception └── XmlContainerNotExistsException.php ├── Rules ├── ContainerInterfacePrivateServiceRule.php └── ContainerInterfaceUnknownServiceRule.php ├── ServiceMap.php └── Type ├── ContainerInterfaceDynamicReturnTypeExtension.php └── ControllerDynamicReturnTypeExtension.php /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.php] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | trim_trailing_whitespace = true 5 | charset = utf-8 6 | indent_style = tab 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Lukáš Unger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | Use [phpstan/phpstan-symfony](https://github.com/phpstan/phpstan-symfony) instead. 4 | 5 | # Symfony extension for PHPStan 6 | 7 | ## What does it do? 8 | 9 | * Provides correct return type for `ContainerInterface::get()` method, 10 | * provides correct return type for `Controller::get()` method, 11 | * notifies you when you try to get an unregistered service from the container, 12 | * notifies you when you try to get a private service from the container. 13 | 14 | ## Installation 15 | 16 | ```sh 17 | composer require --dev lookyman/phpstan-symfony 18 | ``` 19 | 20 | ## Configuration 21 | 22 | Put this into your `phpstan.neon` config: 23 | 24 | ```neon 25 | includes: 26 | - vendor/lookyman/phpstan-symfony/extension.neon 27 | parameters: 28 | symfony: 29 | container_xml_path: %rootDir%/../../../var/cache/dev/appDevDebugProjectContainer.xml # or srcDevDebugProjectContainer.xml for Symfony 4+ 30 | ``` 31 | 32 | ## Limitations 33 | 34 | It can only recognize pure strings or `::class` constants passed into `get()` method. This follows from the nature of static code analysis. 35 | 36 | You have to provide a path to `appDevDebugProjectContainer.xml` or similar xml file describing your container. 37 | 38 | ## Need something? 39 | 40 | I don't use Symfony that often. So it might be entirely possible that something doesn't work here or that it lacks some functionality. If that's the case, **PLEASE DO NOT HESITATE** to open an issue or send a pull request. I will have a look at it and together we'll get you what you need. Thanks. 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lookyman/phpstan-symfony", 3 | "license": "MIT", 4 | "description": "Symfony extension for PHPStan", 5 | "keywords": ["PHPStan", "Symfony"], 6 | "authors": [ 7 | { 8 | "name": "Lukáš Unger", 9 | "email": "looky.msc@gmail.com", 10 | "homepage": "https://lookyman.net" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.1", 15 | "phpstan/phpstan": "^0.9.2" 16 | }, 17 | "require-dev": { 18 | "jakub-onderka/php-parallel-lint": "^0.9.2", 19 | "phpunit/phpunit": "^6.4 || ^7.0", 20 | "phpstan/phpstan-phpunit": "^0.9.0", 21 | "symfony/framework-bundle": "^4.0", 22 | "phpstan/phpstan-strict-rules": "^0.9.0", 23 | "lookyman/coding-standard": "0.1.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Lookyman\\PHPStan\\Symfony\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Lookyman\\PHPStan\\Symfony\\": "tests/" 33 | } 34 | }, 35 | "scripts": { 36 | "lint": "parallel-lint ./src ./tests", 37 | "cs": "phpcs --colors --extensions=php --encoding=utf-8 -sp ./src ./tests", 38 | "tests": "phpunit --coverage-text", 39 | "stan": "phpstan analyse -l max -c ./phpstan.neon ./src ./tests", 40 | "check": [ 41 | "@lint", 42 | "@cs", 43 | "@tests", 44 | "@stan" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | services: 2 | - 3 | class: Lookyman\PHPStan\Symfony\Type\ContainerInterfaceDynamicReturnTypeExtension 4 | tags: [phpstan.broker.dynamicMethodReturnTypeExtension] 5 | - 6 | class: Lookyman\PHPStan\Symfony\Type\ControllerDynamicReturnTypeExtension 7 | tags: [phpstan.broker.dynamicMethodReturnTypeExtension] 8 | - 9 | class: Lookyman\PHPStan\Symfony\Rules\ContainerInterfacePrivateServiceRule 10 | tags: [phpstan.rules.rule] 11 | - 12 | class: Lookyman\PHPStan\Symfony\Rules\ContainerInterfaceUnknownServiceRule 13 | tags: [phpstan.rules.rule] 14 | - Lookyman\PHPStan\Symfony\ServiceMap(%symfony.container_xml_path%) 15 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Exception/XmlContainerNotExistsException.php: -------------------------------------------------------------------------------- 1 | serviceMap = $symfonyServiceMap; 26 | } 27 | 28 | public function getNodeType(): string 29 | { 30 | return MethodCall::class; 31 | } 32 | 33 | public function processNode(Node $node, Scope $scope): array 34 | { 35 | if ($node instanceof MethodCall && $node->name === 'get') { 36 | $type = $scope->getType($node->var); 37 | $baseController = new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\Controller'); 38 | $isInstanceOfController = $type instanceof ThisType && $baseController->isSuperTypeOf($type)->yes(); 39 | $isContainerInterface = $type instanceof ObjectType && $type->getClassName() === 'Symfony\Component\DependencyInjection\ContainerInterface'; 40 | if (($isContainerInterface || $isInstanceOfController) 41 | && isset($node->args[0]) 42 | && $node->args[0] instanceof Arg 43 | ) { 44 | $service = $this->serviceMap->getServiceFromNode($node->args[0]->value, $scope); 45 | if ($service !== \null && !$service['public']) { 46 | return [\sprintf('Service "%s" is private.', $service['id'])]; 47 | } 48 | } 49 | } 50 | return []; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/Rules/ContainerInterfaceUnknownServiceRule.php: -------------------------------------------------------------------------------- 1 | serviceMap = $symfonyServiceMap; 26 | } 27 | 28 | public function getNodeType(): string 29 | { 30 | return MethodCall::class; 31 | } 32 | 33 | public function processNode(Node $node, Scope $scope): array 34 | { 35 | if ($node instanceof MethodCall && $node->name === 'get') { 36 | $type = $scope->getType($node->var); 37 | $baseController = new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\Controller'); 38 | $isInstanceOfController = $type instanceof ThisType && $baseController->isSuperTypeOf($type)->yes(); 39 | $isContainerInterface = $type instanceof ObjectType && $type->getClassName() === 'Symfony\Component\DependencyInjection\ContainerInterface'; 40 | if (($isInstanceOfController || $isContainerInterface) 41 | && isset($node->args[0]) 42 | && $node->args[0] instanceof Arg 43 | ) { 44 | $service = $this->serviceMap->getServiceFromNode($node->args[0]->value, $scope); 45 | if ($service === \null) { 46 | $serviceId = ServiceMap::getServiceIdFromNode($node->args[0]->value, $scope); 47 | if ($serviceId !== null) { 48 | return [\sprintf('Service "%s" is not registered in the container.', $serviceId)]; 49 | } 50 | } 51 | } 52 | } 53 | return []; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/ServiceMap.php: -------------------------------------------------------------------------------- 1 | services = $aliases = []; 25 | /** @var \SimpleXMLElement $def */ 26 | $xml = @\simplexml_load_file($containerXml); 27 | if ($xml === false) { 28 | throw new XmlContainerNotExistsException(\sprintf('Container %s not exists', $containerXml)); 29 | } 30 | foreach ($xml->services->service as $def) { 31 | $attrs = $def->attributes(); 32 | if (!isset($attrs->id)) { 33 | continue; 34 | } 35 | $service = [ 36 | 'id' => (string) $attrs->id, 37 | 'class' => isset($attrs->class) ? (string) $attrs->class : \null, 38 | 'public' => !isset($attrs->public) || (string) $attrs->public !== 'false', 39 | 'synthetic' => isset($attrs->synthetic) && (string) $attrs->synthetic === 'true', 40 | ]; 41 | if (isset($attrs->alias)) { 42 | $aliases[(string) $attrs->id] = \array_merge($service, ['alias' => (string) $attrs->alias]); 43 | } else { 44 | $this->services[(string) $attrs->id] = $service; 45 | } 46 | } 47 | foreach ($aliases as $id => $alias) { 48 | if (\array_key_exists($alias['alias'], $this->services)) { 49 | $this->services[$id] = [ 50 | 'id' => $id, 51 | 'class' => $this->services[$alias['alias']]['class'], 52 | 'public' => $alias['public'], 53 | 'synthetic' => $alias['synthetic'], 54 | ]; 55 | } 56 | } 57 | } 58 | 59 | public function getServiceFromNode(Node $node, Scope $scope): ?array 60 | { 61 | $serviceId = self::getServiceIdFromNode($node, $scope); 62 | return $serviceId !== \null && \array_key_exists($serviceId, $this->services) ? $this->services[$serviceId] : \null; 63 | } 64 | 65 | public static function getServiceIdFromNode(Node $node, Scope $scope): ?string 66 | { 67 | if ($node instanceof String_) { 68 | return $node->value; 69 | } 70 | if ($node instanceof ClassConstFetch && $node->class instanceof Name) { 71 | return $scope->resolveName($node->class); 72 | } 73 | if ($node instanceof Concat) { 74 | $left = self::getServiceIdFromNode($node->left, $scope); 75 | $right = self::getServiceIdFromNode($node->right, $scope); 76 | if ($left !== null && $right !== null) { 77 | return \sprintf('%s%s', $left, $right); 78 | } 79 | } 80 | return \null; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/Type/ContainerInterfaceDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | serviceMap = $symfonyServiceMap; 26 | } 27 | 28 | public function getClass(): string 29 | { 30 | return 'Symfony\Component\DependencyInjection\ContainerInterface'; 31 | } 32 | 33 | public function isMethodSupported(MethodReflection $methodReflection): bool 34 | { 35 | return $methodReflection->getName() === 'get'; 36 | } 37 | 38 | public function getTypeFromMethodCall( 39 | MethodReflection $methodReflection, 40 | MethodCall $methodCall, 41 | Scope $scope 42 | ): Type { 43 | if (isset($methodCall->args[0]) 44 | && $methodCall->args[0] instanceof Arg 45 | ) { 46 | $service = $this->serviceMap->getServiceFromNode($methodCall->args[0]->value, $scope); 47 | if ($service !== \null && !$service['synthetic']) { 48 | return new ObjectType($service['class'] ?? $service['id']); 49 | } 50 | } 51 | return $methodReflection->getReturnType(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Type/ControllerDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | serviceMap = $symfonyServiceMap; 26 | } 27 | 28 | public function getClass(): string 29 | { 30 | return 'Symfony\Bundle\FrameworkBundle\Controller\Controller'; 31 | } 32 | 33 | public function isMethodSupported(MethodReflection $methodReflection): bool 34 | { 35 | return $methodReflection->getName() === 'get'; 36 | } 37 | 38 | public function getTypeFromMethodCall( 39 | MethodReflection $methodReflection, 40 | MethodCall $methodCall, 41 | Scope $scope 42 | ): Type { 43 | if (isset($methodCall->args[0]) 44 | && $methodCall->args[0] instanceof Arg 45 | ) { 46 | $service = $this->serviceMap->getServiceFromNode($methodCall->args[0]->value, $scope); 47 | if ($service !== \null && !$service['synthetic']) { 48 | return new ObjectType($service['class'] ?? $service['id']); 49 | } 50 | } 51 | return $methodReflection->getReturnType(); 52 | } 53 | 54 | } 55 | --------------------------------------------------------------------------------