Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/Drupal/DrupalAutoloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ public function register(Container $container): void
// tags:
// - { name: foo_bar }
// @endcode
if (!isset($serviceDefinition['class']) && class_exists($serviceId)) {
if (!isset($serviceDefinition['class']) && $this->classExists($serviceId)) {
$serviceDefinition['class'] = $serviceId;
}
// @todo sanitize "calls" and "configurator" and "factory"
Expand Down Expand Up @@ -419,4 +419,16 @@ protected function camelize(string $id): string
{
return strtr(ucwords(strtr($id, ['_' => ' ', '.' => '_ ', '\\' => '_ '])), [' ' => '']);
}

private function classExists(string $className): bool
{
try {
return class_exists($className);
} catch (Throwable) {
// Loading the class can fail when it depends on a class from an
// extension that is not available, such as a decorator for an
// optional module registered with decoration_on_invalid: ignore.
return false;
}
}
}
30 changes: 26 additions & 4 deletions src/Drupal/ServiceMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace mglaman\PHPStanDrupal\Drupal;

use Throwable;
use function class_exists;

class ServiceMap
Expand Down Expand Up @@ -36,12 +37,12 @@ public function setDrupalServices(array $drupalServices): void
}

if (isset($serviceDefinition['decorates'])) {
$decorators[$serviceDefinition['decorates']][] = $serviceId;
$decorators[$serviceDefinition['decorates']][$serviceId] = $serviceDefinition['decoration_on_invalid'] ?? 'exception';
}

// @todo support factories
if (!isset($serviceDefinition['class'])) {
if (class_exists($serviceId)) {
if (self::classExists($serviceId)) {
$serviceDefinition['class'] = $serviceId;
} else {
continue;
Expand All @@ -67,15 +68,36 @@ public function setDrupalServices(array $drupalServices): void
}

foreach ($decorators as $decorated_service_id => $services) {
foreach ($services as $dcorating_service_id) {
foreach ($services as $decorating_service_id => $decoration_on_invalid) {
if (!isset(self::$services[$decorated_service_id])) {
// Symfony removes the decorating service from the
// container when the decorated service does not exist and
// decoration_on_invalid is set to ignore.
if ($decoration_on_invalid === 'ignore') {
unset(self::$services[$decorating_service_id]);
}
continue;
}
self::$services[$decorated_service_id]->addDecorator(self::$services[$dcorating_service_id]);
if (!isset(self::$services[$decorating_service_id])) {
continue;
}
self::$services[$decorated_service_id]->addDecorator(self::$services[$decorating_service_id]);
}
}
}

private static function classExists(string $className): bool
{
try {
return class_exists($className);
} catch (Throwable) {
// Loading the class can fail when it depends on a class from an
// extension that is not available, such as a decorator for an
// optional module registered with decoration_on_invalid: ignore.
return false;
}
}

private function resolveParentDefinition(string $parentId, array $serviceDefinition, array $drupalServices): array
{
$parentDefinition = $drupalServices[$parentId] ?? [];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: service_map_broken
type: module
core_version_requirement: ^8 || ^9
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
Drupal\service_map_broken\ExtendsMissingClass:
decorates: missing_module.render_converter
decoration_on_invalid: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Drupal\service_map_broken;

use Drupal\missing_module\RenderConverter;

// The parent class belongs to a module that is not available. Declaring this
// class throws an Error, which the service map must survive.
final class ExtendsMissingClass extends RenderConverter
{

}
50 changes: 50 additions & 0 deletions tests/src/ServiceMapFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,48 @@ public function testFactory(string $id, callable $validator): void
'decorates' => 'unknown',
'class' => 'Drupal\service_map\Override',
],
'ignored_decorator_of_unknown_service' => [
'decorates' => 'unknown',
'decoration_on_invalid' => 'ignore',
'class' => 'Drupal\service_map\Override',
],
]);
$validator($service->getService($id));
}

/**
* @covers \mglaman\PHPStanDrupal\Drupal\ServiceMap::setDrupalServices
* @covers \mglaman\PHPStanDrupal\Drupal\ServiceMap::getService
* @covers \mglaman\PHPStanDrupal\Drupal\ServiceMap::classExists
*/
public function testShorthandServiceWithUnloadableClass(): void
{
$autoloader = static function (string $class): void {
if ($class === 'Drupal\service_map_broken\ExtendsMissingClass') {
require __DIR__ . '/../fixtures/drupal/modules/service_map_broken/src/ExtendsMissingClass.php';
}
};
spl_autoload_register($autoloader);
try {
$service = new ServiceMap();
$service->setDrupalServices([
'decorated_service' => [
'class' => 'Drupal\service_map\Base',
],
'Drupal\service_map_broken\ExtendsMissingClass' => [
'decorates' => 'decorated_service',
'decoration_on_invalid' => 'ignore',
],
]);
} finally {
spl_autoload_unregister($autoloader);
}
self::assertNull($service->getService('Drupal\service_map_broken\ExtendsMissingClass'));
$decorated = $service->getService('decorated_service');
self::assertNotNull($decorated);
self::assertCount(0, $decorated->getDecorators());
}

public static function getServiceProvider(): \Iterator
{
yield [
Expand Down Expand Up @@ -230,6 +268,18 @@ function (DrupalServiceDefinition $service): void {
self::assertEquals(LoggerChannel::class, $service->getClass());
}
];
yield [
'decorating_an_unknown_service',
function (?DrupalServiceDefinition $service): void {
self::assertNotNull($service, 'decorating_an_unknown_service');
},
];
yield [
'ignored_decorator_of_unknown_service',
function (?DrupalServiceDefinition $service): void {
self::assertNull($service, 'ignored_decorator_of_unknown_service');
},
];
yield [
'service_map.base_to_be_decorated',
function (DrupalServiceDefinition $service): void {
Expand Down
Loading