diff --git a/extension.neon b/extension.neon index 277dd5d1..1be664cf 100644 --- a/extension.neon +++ b/extension.neon @@ -297,3 +297,7 @@ services: - class: mglaman\PHPStanDrupal\Drupal\DrupalStubFilesExtension tags: [phpstan.stubFilesExtension] + - + class: mglaman\PHPStanDrupal\Type\ServiceIdTypeNodeResolverExtension + tags: + - phpstan.phpDoc.typeNodeResolverExtension diff --git a/src/Drupal/DrupalAutoloader.php b/src/Drupal/DrupalAutoloader.php index 699a97d6..962dcad1 100644 --- a/src/Drupal/DrupalAutoloader.php +++ b/src/Drupal/DrupalAutoloader.php @@ -191,6 +191,7 @@ class: Drupal\jsonapi\Routing\JsonApiParamEnhancer $service_map = $container->getByType(ServiceMap::class); $service_map->setDrupalServices($this->serviceMap); + ServiceMapStaticAccessor::registerInstance($service_map); if (interface_exists(\PHPUnit\Framework\Test::class) && class_exists('Drupal\TestTools\PhpUnitCompatibility\PhpUnit8\ClassWriter')) { diff --git a/src/Drupal/ServiceMapStaticAccessor.php b/src/Drupal/ServiceMapStaticAccessor.php new file mode 100644 index 00000000..aa6a76dc --- /dev/null +++ b/src/Drupal/ServiceMapStaticAccessor.php @@ -0,0 +1,31 @@ +isAcceptedBy($this, $strictTypes); + } + + if ($type instanceof ConstantStringType) { + $serviceDefinition = ServiceMapStaticAccessor::getInstance()->getService($type->getValue()); + if ($serviceDefinition !== null) { + return TrinaryLogic::createYes(); + } + // Some services may be dynamically defined, so return maybe. + return TrinaryLogic::createMaybe(); + } + + if ($type instanceof self) { + return TrinaryLogic::createYes(); + } + + if ($type instanceof StringType) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createNo(); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + if ($type instanceof ConstantStringType) { + $serviceDefinition = ServiceMapStaticAccessor::getInstance()->getService($type->getValue()); + if ($serviceDefinition !== null) { + return TrinaryLogic::createYes(); + } + // Some services may be dynamically defined, so return maybe. + return TrinaryLogic::createMaybe(); + } + + if ($type instanceof self) { + return TrinaryLogic::createYes(); + } + + if ($type instanceof parent) { + return TrinaryLogic::createMaybe(); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return TrinaryLogic::createNo(); + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties) : \PHPStan\Type\Type + { + return new self(); + } +} diff --git a/src/Type/ServiceIdTypeNodeResolverExtension.php b/src/Type/ServiceIdTypeNodeResolverExtension.php new file mode 100644 index 00000000..d24bf602 --- /dev/null +++ b/src/Type/ServiceIdTypeNodeResolverExtension.php @@ -0,0 +1,21 @@ +__toString() === 'service-id-string') { + return new \mglaman\PHPStanDrupal\Type\ServiceIdType(); + } + + return null; + } +} diff --git a/stubs/Drupal/Drupal.stub b/stubs/Drupal/Drupal.stub new file mode 100644 index 00000000..afa53cae --- /dev/null +++ b/stubs/Drupal/Drupal.stub @@ -0,0 +1,36 @@ +get($id); + } + + /** + * @phpstan-param service-id-string $id + */ + public static function hasService($id): bool { + // Check hasContainer() first in order to always return a Boolean. + return static::hasContainer() && static::getContainer()->has($id); + } + + /** + * @return \Symfony\Component\DependencyInjection\ContainerInterface + */ + public static function getContainer(): \Symfony\Component\DependencyInjection\ContainerInterface { + return static::$container; + } + + public static function hasContainer(): bool { + return static::$container !== NULL; + } + +} diff --git a/tests/src/Type/ServiceIdTypeTest.php b/tests/src/Type/ServiceIdTypeTest.php new file mode 100644 index 00000000..99c4df7b --- /dev/null +++ b/tests/src/Type/ServiceIdTypeTest.php @@ -0,0 +1,64 @@ + [ + new \mglaman\PHPStanDrupal\Type\ServiceIdType(), + new \mglaman\PHPStanDrupal\Type\ServiceIdType(), + TrinaryLogic::createYes(), + ]; + yield 'valid service' => [ + new \mglaman\PHPStanDrupal\Type\ServiceIdType(), + new ConstantStringType('entity_type.manager'), + TrinaryLogic::createYes(), + ]; + yield 'invalid service' => [ + new \mglaman\PHPStanDrupal\Type\ServiceIdType(), + new ConstantStringType('foo.manager'), + TrinaryLogic::createMaybe(), + ]; + yield 'generic string' => [ + new \mglaman\PHPStanDrupal\Type\ServiceIdType(), + new StringType(), + TrinaryLogic::createMaybe(), + ]; + } + + /** + * @dataProvider dataAccepts + */ + public function testAccepts(\mglaman\PHPStanDrupal\Type\ServiceIdType $type, Type $otherType, TrinaryLogic $expectedResult): void + { + $serviceMap = new ServiceMap(); + $serviceMap->setDrupalServices([ + 'entity_type.manager' => [ + 'class' => EntityTypeManager::class + ], + ]); + ServiceMapStaticAccessor::registerInstance($serviceMap); + $actualResult = $type->accepts($otherType, true); + self::assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + +}