Skip to content

Commit 02ea689

Browse files
authored
Merge pull request #9 from mglaman/gh-5
Container service return type support
2 parents 5a7d06c + 26789c5 commit 02ea689

9 files changed

+294
-6
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
],
1111
"require": {
1212
"php": "^7.1",
13-
"phpstan/phpstan": "^0.10.6"
13+
"phpstan/phpstan": "^0.10.6",
14+
"symfony/yaml": "^4.2"
1415
},
1516
"autoload": {
1617
"psr-4": {

extension.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,16 @@ rules:
1919
- PHPStan\Rules\Drupal\GlobalDrupalDependencyInjectionRule
2020
- PHPStan\Rules\Drupal\PluginManager\PluginManagerSetsCacheBackendRule
2121
services:
22+
drupal.serviceMapFactory:
23+
class: PHPStan\Drupal\ServiceMapFactoryInterface
24+
factory: PHPStan\Drupal\ServiceMapFactory(%drupalServiceMap%)
25+
-
26+
class: @drupal.serviceMapFactory::create()
2227
-
2328
class: PHPStan\Type\EntityTypeManagerGetStorageDynamicReturnTypeExtension
2429
arguments:
2530
entityTypeStorageMapping: %drupal.entityTypeStorageMapping%
2631
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
32+
-
33+
class: PHPStan\Type\ServiceDynamicReturnTypeExtension
34+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]

src/DependencyInjection/DrupalExtension.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
use Nette\DI\CompilerExtension;
66
use Nette\DI\Config\Helpers;
7+
use PHPStan\Drupal\ExtensionDiscovery;
78
use PHPStan\Rules\Classes\EnhancedRequireParentConstructCallRule;
89
use PHPStan\Rules\Classes\RequireParentConstructCallRule;
10+
use Symfony\Component\Yaml\Yaml;
911

1012
class DrupalExtension extends CompilerExtension
1113
{
@@ -92,5 +94,75 @@ public function loadConfiguration(): void
9294
$definition->setFactory(EnhancedRequireParentConstructCallRule::class);
9395
}
9496
}
97+
98+
// Build the service definitions...
99+
$extensionDiscovery = new ExtensionDiscovery($this->drupalRoot);
100+
$extensionDiscovery->setProfileDirectories([]);
101+
$profiles = $extensionDiscovery->scan('profile');
102+
$profile_directories = array_map(function ($profile) {
103+
return $profile->getPath();
104+
}, $profiles);
105+
$extensionDiscovery->setProfileDirectories($profile_directories);
106+
107+
108+
$serviceYamls = [
109+
'core' => $this->drupalRoot . '/core/core.services.yml',
110+
];
111+
$serviceClassProviders = [
112+
'core' => 'Drupal\Core\CoreServiceProvider',
113+
];
114+
115+
foreach ($extensionDiscovery->scan('module') as $extension) {
116+
$module_dir = $this->drupalRoot . '/' . $extension->getPath();
117+
$moduleName = $extension->getName();
118+
$servicesFileName = $module_dir . '/' . $moduleName . '.services.yml';
119+
if (file_exists($servicesFileName)) {
120+
$serviceYamls[$moduleName] = $servicesFileName;
121+
}
122+
123+
$camelized = $this->camelize($extension->getName());
124+
$name = "{$camelized}ServiceProvider";
125+
$class = "Drupal\\{$moduleName}\\{$name}";
126+
127+
if (class_exists($class)) {
128+
$serviceClassProviders[$moduleName] = $class;
129+
}
130+
}
131+
132+
foreach ($serviceYamls as $extension => $serviceYaml) {
133+
$yaml = Yaml::parseFile($serviceYaml);
134+
// Weed out service files which only provide parameters.
135+
if (!isset($yaml['services']) || !is_array($yaml['services'])) {
136+
continue;
137+
}
138+
foreach ($yaml['services'] as $serviceId => $serviceDefinition) {
139+
// Prevent \Nette\DI\ContainerBuilder::completeStatement from array_walk_recursive into the arguments
140+
// and thinking these are real services for PHPStan's container.
141+
if (isset($serviceDefinition['arguments']) && is_array($serviceDefinition['arguments'])) {
142+
array_walk($serviceDefinition['arguments'], function (&$argument) {
143+
$argument = str_replace('@', '', $argument);
144+
});
145+
}
146+
unset($serviceDefinition['tags']);
147+
// @todo sanitize "calls" and "configurator" and "factory"
148+
/**
149+
jsonapi.params.enhancer:
150+
class: Drupal\jsonapi\Routing\JsonApiParamEnhancer
151+
calls:
152+
- [setContainer, ['@service_container']]
153+
tags:
154+
- { name: route_enhancer }
155+
*/
156+
unset($serviceDefinition['calls']);
157+
unset($serviceDefinition['configurator']);
158+
unset($serviceDefinition['factory']);
159+
$builder->parameters['drupalServiceMap'][$serviceId] = $serviceDefinition;
160+
}
161+
}
162+
}
163+
164+
protected function camelize(string $id): string
165+
{
166+
return strtr(ucwords(strtr($id, ['_' => ' ', '.' => '_ ', '\\' => '_ '])), [' ' => '']);
95167
}
96168
}

src/Drupal/Bootstrap.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,11 @@ protected function addCoreNamespaces(): void
163163

164164
// Add core test namespaces.
165165
$core_tests_dir = $this->drupalRoot . '/core/tests';
166-
$this->namespaces['Drupal\\Tests'] = $core_tests_dir;
167-
$this->namespaces['Drupal\\TestSite'] = $core_tests_dir;
168-
$this->namespaces['Drupal\\KernelTests'] = $core_tests_dir;
169-
$this->namespaces['Drupal\\FunctionalTests'] = $core_tests_dir;
170-
$this->namespaces['Drupal\\FunctionalJavascriptTests'] = $core_tests_dir;
166+
$this->autoloader->add('Drupal\\Tests', $core_tests_dir);
167+
$this->autoloader->add('Drupal\\TestSite', $core_tests_dir);
168+
$this->autoloader->add('Drupal\\KernelTests', $core_tests_dir);
169+
$this->autoloader->add('Drupal\\FunctionalTests', $core_tests_dir);
170+
$this->autoloader->add('Drupal\\FunctionalJavascriptTests', $core_tests_dir);
171171
}
172172
protected function addModuleNamespaces(): void
173173
{
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Drupal;
4+
5+
class DrupalServiceDefinition
6+
{
7+
8+
/**
9+
* @var string
10+
*/
11+
private $id;
12+
13+
/**
14+
* @var string|null
15+
*/
16+
private $class;
17+
18+
/**
19+
* @var bool
20+
*/
21+
private $public;
22+
23+
/**
24+
* @var string|null
25+
*/
26+
private $alias;
27+
28+
public function __construct(string $id, ?string $class, bool $public = true, ?string $alias = null)
29+
{
30+
$this->id = $id;
31+
$this->class = $class;
32+
$this->public = $public;
33+
$this->alias = $alias;
34+
}
35+
36+
/**
37+
* @return string
38+
*/
39+
public function getId(): string
40+
{
41+
return $this->id;
42+
}
43+
44+
/**
45+
* @return string|null
46+
*/
47+
public function getClass(): ?string
48+
{
49+
return $this->class;
50+
}
51+
52+
/**
53+
* @return bool
54+
*/
55+
public function isPublic(): bool
56+
{
57+
return $this->public;
58+
}
59+
60+
/**
61+
* @return string|null
62+
*/
63+
public function getAlias(): ?string
64+
{
65+
return $this->alias;
66+
}
67+
}

src/Drupal/ServiceMap.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Drupal;
4+
5+
class ServiceMap
6+
{
7+
/** @var \PHPStan\Drupal\DrupalServiceDefinition[] */
8+
private $services;
9+
10+
/**
11+
* ServiceMap constructor.
12+
* @param array $drupalServices
13+
*/
14+
public function __construct(array $drupalServices)
15+
{
16+
foreach ($drupalServices as $serviceId => $serviceDefinition) {
17+
// @todo support factories
18+
if (!isset($serviceDefinition['class'])) {
19+
continue;
20+
}
21+
$this->services[$serviceId] = new DrupalServiceDefinition(
22+
$serviceId,
23+
$serviceDefinition['class'],
24+
$serviceDefinition['public'] ?? true,
25+
$serviceDefinition['alias'] ?? null
26+
);
27+
}
28+
}
29+
30+
/**
31+
* @return DrupalServiceDefinition[]
32+
*/
33+
public function getServices(): array
34+
{
35+
return $this->services;
36+
}
37+
38+
public function getService(string $id): ?DrupalServiceDefinition
39+
{
40+
return $this->services[$id] ?? null;
41+
}
42+
}

src/Drupal/ServiceMapFactory.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Drupal;
4+
5+
class ServiceMapFactory implements ServiceMapFactoryInterface
6+
{
7+
/**
8+
* @var array
9+
*/
10+
private $drupalServices;
11+
12+
public function __construct(array $drupalServiceMap = [])
13+
{
14+
$this->drupalServices = $drupalServiceMap;
15+
}
16+
17+
public function create(): ServiceMap
18+
{
19+
return new ServiceMap($this->drupalServices);
20+
}
21+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Drupal;
4+
5+
interface ServiceMapFactoryInterface
6+
{
7+
public function create(): ServiceMap;
8+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PhpParser\Node\Scalar\String_;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Drupal\DrupalServiceDefinition;
9+
use PHPStan\Drupal\ServiceMap;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ParametersAcceptorSelector;
12+
use PHPStan\ShouldNotHappenException;
13+
use PHPStan\Type\Constant\ConstantBooleanType;
14+
15+
class ServiceDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
16+
{
17+
/**
18+
* @var ServiceMap
19+
*/
20+
private $serviceMap;
21+
22+
public function __construct(ServiceMap $serviceMap)
23+
{
24+
$this->serviceMap = $serviceMap;
25+
}
26+
27+
public function getClass(): string
28+
{
29+
return 'Symfony\Component\DependencyInjection\ContainerInterface';
30+
}
31+
32+
public function isMethodSupported(MethodReflection $methodReflection): bool
33+
{
34+
return in_array($methodReflection->getName(), ['get', 'has'], true);
35+
}
36+
37+
public function getTypeFromMethodCall(
38+
MethodReflection $methodReflection,
39+
MethodCall $methodCall,
40+
Scope $scope
41+
): Type {
42+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
43+
if (!isset($methodCall->args[0])) {
44+
return $returnType;
45+
}
46+
47+
$arg1 = $methodCall->args[0]->value;
48+
if (!$arg1 instanceof String_) {
49+
// @todo determine what these types are.
50+
return $returnType;
51+
}
52+
53+
$serviceId = $arg1->value;
54+
55+
if ($methodReflection->getName() === 'get') {
56+
$service = $this->serviceMap->getService($serviceId);
57+
if ($service instanceof DrupalServiceDefinition) {
58+
return new ObjectType($service->getClass() ?? $serviceId);
59+
}
60+
return $returnType;
61+
}
62+
63+
if ($methodReflection->getName() === 'has') {
64+
return new ConstantBooleanType($this->serviceMap->getService($serviceId) instanceof DrupalServiceDefinition);
65+
}
66+
67+
throw new ShouldNotHappenException();
68+
}
69+
}

0 commit comments

Comments
 (0)