class DrupalDebugClassLoader
Extends Symfony's DebugClassLoader for Drupal-aware vendor boundaries.
Symfony's DebugClassLoader uses the first namespace segment as the vendor boundary for return type deprecation checks. Since all Drupal code shares "Drupal\" as the first segment, cross-extension return type deprecations (e.g., contrib extending core) are never triggered.
This subclass treats each Drupal extension (the second namespace segment) as the vendor boundary, enabling return type deprecation notices when an extension overrides a method from a different extension without adding native return types.
@internal
Hierarchy
- class \Drupal\TestTools\ErrorHandler\DrupalDebugClassLoader extends \Symfony\Component\ErrorHandler\DebugClassLoader
Expanded class hierarchy of DrupalDebugClassLoader
2 files declare their use of DrupalDebugClassLoader
- bootstrap.php in core/
tests/ bootstrap.php - DrupalDebugClassLoaderTest.php in core/
tests/ Drupal/ Tests/ TestTools/ ErrorHandler/ DrupalDebugClassLoaderTest.php
File
-
core/
tests/ Drupal/ TestTools/ ErrorHandler/ DrupalDebugClassLoader.php, line 24
Namespace
Drupal\TestTools\ErrorHandlerView source
class DrupalDebugClassLoader extends DebugClassLoader {
/**
* Cached ReflectionProperty for DebugClassLoader::$returnTypes.
*/
private static \ReflectionProperty $returnTypesProperty;
/**
* Cached ReflectionProperty for DebugClassLoader::$patchTypes.
*/
private static \ReflectionProperty $patchTypesProperty;
/**
* {@inheritdoc}
*/
public function checkAnnotations(\ReflectionClass $refl, string $class) : array {
$deprecations = parent::checkAnnotations($refl, $class);
// Only process non-trait Drupal classes.
if (!str_starts_with($class, 'Drupal\\') || trait_exists($class, FALSE)) {
return $deprecations;
}
self::$returnTypesProperty ??= new \ReflectionProperty(DebugClassLoader::class, 'returnTypes');
$returnTypes = self::$returnTypesProperty->getValue()[$class] ?? [];
self::$patchTypesProperty ??= new \ReflectionProperty(DebugClassLoader::class, 'patchTypes');
if (!$returnTypes || empty(self::$patchTypesProperty->getValue($this)['deprecations'])) {
return $deprecations;
}
$classExtension = self::getExtensionName($class);
$className = str_contains($class, "@anonymous\x00") ? ((get_parent_class($class) ?: key(class_implements($class))) ?: 'class') . '@anonymous' : $class;
foreach ($returnTypes as $method => $returnTypeData) {
[$normalizedType, , $declaringClass] = $returnTypeData;
// Skip if no cross-extension boundary: empty declaring class (magic
// methods), same class (own @return), or same/non-Drupal extension.
$declaringExtension = self::getExtensionName($declaringClass);
if ($declaringClass === '' || $declaringClass === $class || $declaringExtension === NULL || $classExtension === $declaringExtension) {
continue;
}
// Skip if not actually overridden, or already has a native return type.
$methodRefl = $refl->getMethod($method);
if ($methodRefl->class !== $class || $methodRefl->hasReturnType()) {
continue;
}
// Skip if the method's docblock contains @deprecated or @return.
$docComment = $methodRefl->getDocComment();
if ($docComment !== FALSE && (str_contains($docComment, '@deprecated') || str_contains($docComment, '@return'))) {
continue;
}
$deprecations[] = \sprintf('Method "%s::%s()" might add "%s" as a native return type declaration in the future. Do the same in %s "%s" now to avoid errors or add an explicit @return annotation to suppress this message.', $declaringClass, $method, $normalizedType, interface_exists($declaringClass) ? 'implementation' : 'child class', $className);
}
return $deprecations;
}
/**
* Extracts the Drupal extension name from a fully qualified class name.
*
* @param string $class
* The fully qualified class name.
*
* @return string|null
* The extension name, or NULL for non-Drupal classes.
*/
private static function getExtensionName(string $class) : ?string {
if (!str_starts_with($class, 'Drupal\\')) {
return NULL;
}
$parts = explode('\\', $class);
if (isset($parts[2]) && $parts[1] === 'Tests') {
return $parts[2];
}
return $parts[1] ?? NULL;
}
}
Members
| Title Sort descending | Modifiers | Object type | Summary |
|---|---|---|---|
| DrupalDebugClassLoader::$patchTypesProperty | private static | property | Cached ReflectionProperty for DebugClassLoader::$patchTypes. |
| DrupalDebugClassLoader::$returnTypesProperty | private static | property | Cached ReflectionProperty for DebugClassLoader::$returnTypes. |
| DrupalDebugClassLoader::checkAnnotations | public | function | |
| DrupalDebugClassLoader::getExtensionName | private static | function | Extracts the Drupal extension name from a fully qualified class name. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.