Skip to content

Commit

Permalink
Add service-id-string type
Browse files Browse the repository at this point in the history
  • Loading branch information
mglaman committed Jun 1, 2022
1 parent 91491a0 commit 9c3de3b
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 0 deletions.
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,7 @@ services:
-
class: mglaman\PHPStanDrupal\Drupal\DrupalStubFilesExtension
tags: [phpstan.stubFilesExtension]
-
class: mglaman\PHPStanDrupal\Type\ServiceIdTypeNodeResolverExtension
tags:
- phpstan.phpDoc.typeNodeResolverExtension
1 change: 1 addition & 0 deletions src/Drupal/DrupalAutoloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down
31 changes: 31 additions & 0 deletions src/Drupal/ServiceMapStaticAccessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Drupal;

use PHPStan\ShouldNotHappenException;

final class ServiceMapStaticAccessor
{
/**
* @var \mglaman\PHPStanDrupal\Drupal\ServiceMap
*/
private static $instance;

private function __construct()
{
}

public static function registerInstance(ServiceMap $serviceMap): void
{
self::$instance = $serviceMap;
}

public static function getInstance(): ServiceMap
{
if (self::$instance === null) {
throw new ShouldNotHappenException();
}

return self::$instance;
}
}
85 changes: 85 additions & 0 deletions src/Type/ServiceIdType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Type;

use mglaman\PHPStanDrupal\Drupal\ServiceMapStaticAccessor;
use PHPStan\TrinaryLogic;
use PHPStan\Type\CompoundType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;

final class ServiceIdType extends \PHPStan\Type\StringType
{
public function __construct()
{
parent::__construct();
}

public function describe(VerbosityLevel $level): string
{
return 'service-id-string';
}

public function accepts(Type $type, bool $strictTypes): TrinaryLogic
{
if ($type instanceof CompoundType) {
return $type->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();
}

}
21 changes: 21 additions & 0 deletions src/Type/ServiceIdTypeNodeResolverExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Type;

use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\TypeNodeResolverExtension;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;

final class ServiceIdTypeNodeResolverExtension implements TypeNodeResolverExtension
{

public function resolve(TypeNode $typeNode, NameScope $nameScope): ?\PHPStan\Type\Type
{
if ($typeNode instanceof IdentifierTypeNode && $typeNode->__toString() === 'service-id-string') {
return new \mglaman\PHPStanDrupal\Type\ServiceIdType();
}

return null;
}
}
36 changes: 36 additions & 0 deletions stubs/Drupal/Drupal.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

class Drupal {

/**
* @var \Symfony\Component\DependencyInjection\ContainerInterface|null
*/
protected static $container;

/**
* @phpstan-param service-id-string $id
*/
public static function service($id) {
return static::getContainer()->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;
}

}
64 changes: 64 additions & 0 deletions tests/src/Type/ServiceIdTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Tests\Type;

use Drupal\Core\Entity\EntityTypeManager;
use mglaman\PHPStanDrupal\Drupal\ServiceMap;
use mglaman\PHPStanDrupal\Drupal\ServiceMapStaticAccessor;
use mglaman\PHPStanDrupal\Tests\AdditionalConfigFilesTrait;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;

final class ServiceIdTypeTest extends PHPStanTestCase {

use AdditionalConfigFilesTrait;

public function dataAccepts(): iterable
{
yield 'self' => [
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())),
);
}

}

0 comments on commit 9c3de3b

Please sign in to comment.