Skip to content

Commit 9c3de3b

Browse files
committed
Add service-id-string type
1 parent 91491a0 commit 9c3de3b

File tree

7 files changed

+242
-0
lines changed

7 files changed

+242
-0
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,7 @@ services:
297297
-
298298
class: mglaman\PHPStanDrupal\Drupal\DrupalStubFilesExtension
299299
tags: [phpstan.stubFilesExtension]
300+
-
301+
class: mglaman\PHPStanDrupal\Type\ServiceIdTypeNodeResolverExtension
302+
tags:
303+
- phpstan.phpDoc.typeNodeResolverExtension

src/Drupal/DrupalAutoloader.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class: Drupal\jsonapi\Routing\JsonApiParamEnhancer
191191

192192
$service_map = $container->getByType(ServiceMap::class);
193193
$service_map->setDrupalServices($this->serviceMap);
194+
ServiceMapStaticAccessor::registerInstance($service_map);
194195

195196
if (interface_exists(\PHPUnit\Framework\Test::class)
196197
&& class_exists('Drupal\TestTools\PhpUnitCompatibility\PhpUnit8\ClassWriter')) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Drupal;
4+
5+
use PHPStan\ShouldNotHappenException;
6+
7+
final class ServiceMapStaticAccessor
8+
{
9+
/**
10+
* @var \mglaman\PHPStanDrupal\Drupal\ServiceMap
11+
*/
12+
private static $instance;
13+
14+
private function __construct()
15+
{
16+
}
17+
18+
public static function registerInstance(ServiceMap $serviceMap): void
19+
{
20+
self::$instance = $serviceMap;
21+
}
22+
23+
public static function getInstance(): ServiceMap
24+
{
25+
if (self::$instance === null) {
26+
throw new ShouldNotHappenException();
27+
}
28+
29+
return self::$instance;
30+
}
31+
}

src/Type/ServiceIdType.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Type;
4+
5+
use mglaman\PHPStanDrupal\Drupal\ServiceMapStaticAccessor;
6+
use PHPStan\TrinaryLogic;
7+
use PHPStan\Type\CompoundType;
8+
use PHPStan\Type\Constant\ConstantStringType;
9+
use PHPStan\Type\StringType;
10+
use PHPStan\Type\Type;
11+
use PHPStan\Type\VerbosityLevel;
12+
13+
final class ServiceIdType extends \PHPStan\Type\StringType
14+
{
15+
public function __construct()
16+
{
17+
parent::__construct();
18+
}
19+
20+
public function describe(VerbosityLevel $level): string
21+
{
22+
return 'service-id-string';
23+
}
24+
25+
public function accepts(Type $type, bool $strictTypes): TrinaryLogic
26+
{
27+
if ($type instanceof CompoundType) {
28+
return $type->isAcceptedBy($this, $strictTypes);
29+
}
30+
31+
if ($type instanceof ConstantStringType) {
32+
$serviceDefinition = ServiceMapStaticAccessor::getInstance()->getService($type->getValue());
33+
if ($serviceDefinition !== null) {
34+
return TrinaryLogic::createYes();
35+
}
36+
// Some services may be dynamically defined, so return maybe.
37+
return TrinaryLogic::createMaybe();
38+
}
39+
40+
if ($type instanceof self) {
41+
return TrinaryLogic::createYes();
42+
}
43+
44+
if ($type instanceof StringType) {
45+
return TrinaryLogic::createMaybe();
46+
}
47+
48+
return TrinaryLogic::createNo();
49+
}
50+
51+
public function isSuperTypeOf(Type $type): TrinaryLogic
52+
{
53+
if ($type instanceof ConstantStringType) {
54+
$serviceDefinition = ServiceMapStaticAccessor::getInstance()->getService($type->getValue());
55+
if ($serviceDefinition !== null) {
56+
return TrinaryLogic::createYes();
57+
}
58+
// Some services may be dynamically defined, so return maybe.
59+
return TrinaryLogic::createMaybe();
60+
}
61+
62+
if ($type instanceof self) {
63+
return TrinaryLogic::createYes();
64+
}
65+
66+
if ($type instanceof parent) {
67+
return TrinaryLogic::createMaybe();
68+
}
69+
70+
if ($type instanceof CompoundType) {
71+
return $type->isSubTypeOf($this);
72+
}
73+
74+
return TrinaryLogic::createNo();
75+
}
76+
77+
/**
78+
* @param mixed[] $properties
79+
*/
80+
public static function __set_state(array $properties) : \PHPStan\Type\Type
81+
{
82+
return new self();
83+
}
84+
85+
}
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 mglaman\PHPStanDrupal\Type;
4+
5+
use PHPStan\Analyser\NameScope;
6+
use PHPStan\PhpDoc\TypeNodeResolverExtension;
7+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
8+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
9+
10+
final class ServiceIdTypeNodeResolverExtension implements TypeNodeResolverExtension
11+
{
12+
13+
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?\PHPStan\Type\Type
14+
{
15+
if ($typeNode instanceof IdentifierTypeNode && $typeNode->__toString() === 'service-id-string') {
16+
return new \mglaman\PHPStanDrupal\Type\ServiceIdType();
17+
}
18+
19+
return null;
20+
}
21+
}

stubs/Drupal/Drupal.stub

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
class Drupal {
4+
5+
/**
6+
* @var \Symfony\Component\DependencyInjection\ContainerInterface|null
7+
*/
8+
protected static $container;
9+
10+
/**
11+
* @phpstan-param service-id-string $id
12+
*/
13+
public static function service($id) {
14+
return static::getContainer()->get($id);
15+
}
16+
17+
/**
18+
* @phpstan-param service-id-string $id
19+
*/
20+
public static function hasService($id): bool {
21+
// Check hasContainer() first in order to always return a Boolean.
22+
return static::hasContainer() && static::getContainer()->has($id);
23+
}
24+
25+
/**
26+
* @return \Symfony\Component\DependencyInjection\ContainerInterface
27+
*/
28+
public static function getContainer(): \Symfony\Component\DependencyInjection\ContainerInterface {
29+
return static::$container;
30+
}
31+
32+
public static function hasContainer(): bool {
33+
return static::$container !== NULL;
34+
}
35+
36+
}

tests/src/Type/ServiceIdTypeTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Tests\Type;
4+
5+
use Drupal\Core\Entity\EntityTypeManager;
6+
use mglaman\PHPStanDrupal\Drupal\ServiceMap;
7+
use mglaman\PHPStanDrupal\Drupal\ServiceMapStaticAccessor;
8+
use mglaman\PHPStanDrupal\Tests\AdditionalConfigFilesTrait;
9+
use PHPStan\Testing\PHPStanTestCase;
10+
use PHPStan\TrinaryLogic;
11+
use PHPStan\Type\Constant\ConstantStringType;
12+
use PHPStan\Type\StringType;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\VerbosityLevel;
15+
16+
final class ServiceIdTypeTest extends PHPStanTestCase {
17+
18+
use AdditionalConfigFilesTrait;
19+
20+
public function dataAccepts(): iterable
21+
{
22+
yield 'self' => [
23+
new \mglaman\PHPStanDrupal\Type\ServiceIdType(),
24+
new \mglaman\PHPStanDrupal\Type\ServiceIdType(),
25+
TrinaryLogic::createYes(),
26+
];
27+
yield 'valid service' => [
28+
new \mglaman\PHPStanDrupal\Type\ServiceIdType(),
29+
new ConstantStringType('entity_type.manager'),
30+
TrinaryLogic::createYes(),
31+
];
32+
yield 'invalid service' => [
33+
new \mglaman\PHPStanDrupal\Type\ServiceIdType(),
34+
new ConstantStringType('foo.manager'),
35+
TrinaryLogic::createMaybe(),
36+
];
37+
yield 'generic string' => [
38+
new \mglaman\PHPStanDrupal\Type\ServiceIdType(),
39+
new StringType(),
40+
TrinaryLogic::createMaybe(),
41+
];
42+
}
43+
44+
/**
45+
* @dataProvider dataAccepts
46+
*/
47+
public function testAccepts(\mglaman\PHPStanDrupal\Type\ServiceIdType $type, Type $otherType, TrinaryLogic $expectedResult): void
48+
{
49+
$serviceMap = new ServiceMap();
50+
$serviceMap->setDrupalServices([
51+
'entity_type.manager' => [
52+
'class' => EntityTypeManager::class
53+
],
54+
]);
55+
ServiceMapStaticAccessor::registerInstance($serviceMap);
56+
$actualResult = $type->accepts($otherType, true);
57+
self::assertSame(
58+
$expectedResult->describe(),
59+
$actualResult->describe(),
60+
sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())),
61+
);
62+
}
63+
64+
}

0 commit comments

Comments
 (0)