Skip to content

Commit 4da2f69

Browse files
committed
Add the service map
1 parent 39a87aa commit 4da2f69

File tree

8 files changed

+254
-3
lines changed

8 files changed

+254
-3
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: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPStan\Drupal\ExtensionDiscovery;
88
use PHPStan\Rules\Classes\EnhancedRequireParentConstructCallRule;
99
use PHPStan\Rules\Classes\RequireParentConstructCallRule;
10+
use Symfony\Component\Yaml\Yaml;
1011

1112
class DrupalExtension extends CompilerExtension
1213
{
@@ -103,12 +104,65 @@ public function loadConfiguration(): void
103104
}, $profiles);
104105
$extensionDiscovery->setProfileDirectories($profile_directories);
105106

107+
108+
$serviceYamls = [
109+
'core' => $this->drupalRoot . '/core/core.services.yml',
110+
];
111+
$serviceClassProviders = [
112+
'core' => 'Drupal\Core\CoreServiceProvider',
113+
];
114+
106115
foreach ($extensionDiscovery->scan('module') as $extension) {
107116
$module_dir = $this->drupalRoot . '/' . $extension->getPath();
108-
$servicesFileName = $module_dir . '/' . $extension->getName() . '.services.yml';
117+
$moduleName = $extension->getName();
118+
$servicesFileName = $module_dir . '/' . $moduleName . '.services.yml';
109119
if (file_exists($servicesFileName)) {
110-
// @todo load and parse, push basic definitions into container parameters
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;
111129
}
112130
}
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($id)
165+
{
166+
return strtr(ucwords(strtr($id, ['_' => ' ', '.' => '_ ', '\\' => '_ '])), [' ' => '']);
113167
}
114168
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Drupal;
4+
5+
class DrupalServiceDefinition
6+
{
7+
private $id;
8+
private $class;
9+
private $public;
10+
private $alias;
11+
12+
public function __construct(string $id, ?string $class, bool $public = true, ?string $alias = null)
13+
{
14+
$this->id = $id;
15+
$this->class = $class;
16+
$this->public = $public;
17+
$this->alias = $alias;
18+
}
19+
20+
/**
21+
* @return string
22+
*/
23+
public function getId(): string
24+
{
25+
return $this->id;
26+
}
27+
28+
/**
29+
* @return string|null
30+
*/
31+
public function getClass(): ?string
32+
{
33+
return $this->class;
34+
}
35+
36+
/**
37+
* @return bool
38+
*/
39+
public function isPublic(): bool
40+
{
41+
return $this->public;
42+
}
43+
44+
/**
45+
* @return string|null
46+
*/
47+
public function getAlias(): ?string
48+
{
49+
return $this->alias;
50+
}
51+
52+
}
53+

src/Drupal/ServiceMap.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
43+
}

src/Drupal/ServiceMapFactory.php

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

0 commit comments

Comments
 (0)