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\ErrorHandler
View 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.