test_helpers-1.0.0-alpha6/src/TestHelpers.php

src/TestHelpers.php
<?php

namespace Drupal\test_helpers;

use Drupal\Component\Annotation\Doctrine\SimpleAnnotationReader;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Database\Query\ConditionInterface as DatabaseQueryConditionInterface;
use Drupal\Core\Database\Query\SelectInterface as DatabaseSelectInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Entity\Query\ConditionInterface as EntityQueryConditionInterface;
use Drupal\Core\Entity\Query\QueryInterface as EntityQueryInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
use Drupal\Core\Language\Language;
use Drupal\Core\Logger\LoggerChannelFactory;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Core\Plugin\Discovery\AttributeClassDiscovery;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\test_helpers\Stub\CacheContextsManagerStub;
use Drupal\test_helpers\Stub\CacheFactoryStub;
use Drupal\test_helpers\Stub\ConfigFactoryStub;
use Drupal\test_helpers\Stub\ConfigurableLanguageManagerStub;
use Drupal\test_helpers\Stub\ContainerAwareEventDispatcherStub;
use Drupal\test_helpers\Stub\DatabaseConnectionStub\Connection;
use Drupal\test_helpers\Stub\DatabaseStorageStub;
use Drupal\test_helpers\Stub\DateFormatterStub;
use Drupal\test_helpers\Stub\DrupalKernelStub;
use Drupal\test_helpers\Stub\EntityBundleListenerStub;
use Drupal\test_helpers\Stub\EntityFieldManagerStub;
use Drupal\test_helpers\Stub\EntityTypeBundleInfoStub;
use Drupal\test_helpers\Stub\EntityTypeManagerStub;
use Drupal\test_helpers\Stub\HttpClientFactoryStub;
use Drupal\test_helpers\Stub\KeyValueFactoryStub;
use Drupal\test_helpers\Stub\LanguageDefaultStub;
use Drupal\test_helpers\Stub\LoggerChannelFactoryStub;
use Drupal\test_helpers\Stub\MemoryBackendStub;
use Drupal\test_helpers\Stub\ModuleHandlerStub;
use Drupal\test_helpers\Stub\PermissionHandlerStub;
use Drupal\test_helpers\Stub\RendererStub;
use Drupal\test_helpers\Stub\RequestStackStub;
use Drupal\test_helpers\Stub\RouteProviderStub;
use Drupal\test_helpers\Stub\SettingsStubFactory;
use Drupal\test_helpers\Stub\TypedDataManagerStub;
use Drupal\test_helpers\Stub\UrlGeneratorStub;
use Drupal\test_helpers\StubFactory\EntityStubFactory;
use Drupal\test_helpers\StubFactory\FieldItemListStubFactory;
use Drupal\test_helpers\lib\MockedFunctionCalls;
use Drupal\test_helpers\lib\MockedFunctionStorage;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\MockObject\Builder\InvocationMocker;
use PHPUnit\Framework\MockObject\MethodNameNotConfiguredException;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Container\ContainerInterface;
use Symfony\Component\Yaml\Yaml;

// Some constants are required to be defined for calling Drupal API functions
// from 'core/modules/system/system.module' file.
// - DRUPAL_DISABLED
// - DRUPAL_OPTIONAL
// - DRUPAL_REQUIRED
// - REGIONS_VISIBLE
// - REGIONS_ALL
// Redefining them here to not include the whole file.
defined('DRUPAL_DISABLED') || define('DRUPAL_DISABLED', 0);
defined('DRUPAL_OPTIONAL') || define('DRUPAL_OPTIONAL', 1);
defined('DRUPAL_REQUIRED') || define('DRUPAL_REQUIRED', 2);
defined('REGIONS_VISIBLE') || define('REGIONS_VISIBLE', 'visible');
defined('REGIONS_ALL') || define('REGIONS_ALL', 'all');

// And some more constants from '/core/includes/common.inc' file.
// - SAVED_NEW
// - SAVED_UPDATED
// Redefining them here to not include the whole file.
defined('SAVED_NEW') || define('SAVED_NEW', 1);
defined('SAVED_UPDATED') || define('SAVED_UPDATED', 2);

/**
 * The main API functions for Test Helpers.
 *
 * Use only static calls like `TestHelpers::method()`.
 *
 * @package TestHelpers
 */
class TestHelpers {

  /**
   * An array of implemented custom stubs for services.
   *
   * Key: a service name.
   * Value: a service class, or a callback function to initialize an instance
   * as array in format "[className, functionName]".
   *
   * @internal For internal usage only.
   */
  private const SERVICES_CUSTOM_STUBS = [
    'cache_contexts_manager' => CacheContextsManagerStub::class,
    'cache.bootstrap' => MemoryBackendStub::class,
    'cache.config' => MemoryBackendStub::class,
    'cache_factory' => CacheFactoryStub::class,
    'class_resolver' => [self::class, 'getClassResolverStub'],
    'config.factory' => ConfigFactoryStub::class,
    'config.storage.active' => DatabaseStorageStub::class,
    'config.storage.snapshot' => DatabaseStorageStub::class,
    'database' => [Connection::class, 'stubGetConnection'],
    'date.formatter' => DateFormatterStub::class,
    'entity_bundle.listener' => EntityBundleListenerStub::class,
    'entity_field.manager' => EntityFieldManagerStub::class,
    'entity_type.bundle.info' => EntityTypeBundleInfoStub::class,
    'entity_type.manager' => EntityTypeManagerStub::class,
    'event_dispatcher' => ContainerAwareEventDispatcherStub::class,
    'http_client_factory' => HttpClientFactoryStub::class,
    'keyvalue' => KeyValueFactoryStub::class,
    'keyvalue.database' => KeyValueMemoryFactory::class,
    'kernel' => DrupalKernelStub::class,
    'language_manager' => ConfigurableLanguageManagerStub::class,
    'language.default' => LanguageDefaultStub::class,
    'logger.factory' => LoggerChannelFactoryStub::class,
    'module_handler' => ModuleHandlerStub::class,
    'renderer' => RendererStub::class,
    'request_stack' => RequestStackStub::class,
    'router.route_provider' => RouteProviderStub::class,
    'settings' => [SettingsStubFactory::class, 'get'],
    'string_translation' => [self::class, 'getStringTranslationStub'],
    'typed_data_manager' => TypedDataManagerStub::class,
    'url_generator.non_bubbling' => UrlGeneratorStub::class,
    'user.permissions' => PermissionHandlerStub::class,
  ];

  /**
   * A list of core services that can be initialized automatically.
   *
   * @internal For internal usage only.
   */
  private const SERVICES_CORE_INIT = [
    'cache_tags.invalidator',
    'cache.backend.memory',
    'cache.backend.database',
    'cache.bootstrap',
    'cache.config',
    'cache.data',
    'cache.default',
    'cache.discovery',
    'cache.entity',
    'cache.menu',
    'cache.render',
    'cache.static',
    'config.storage',
    'current_user',
    'path.current',
    'database.replica_kill_switch',
    'datetime.time',
    'entity.memory_cache',
    'entity.repository',
    'http_handler_stack',
    'link_generator',
    'logger.factory',
    'messenger',
    'pager.manager',
    'pager.parameters',
    'path_processor_manager',
    'request_stack',
    'router.no_access_checks',
    'session.flash_bag',
    'settings',
    'state',
    'token',
    'transliteration',
    'unrouted_url_assembler',
    'url_generator',
    'uuid',
  ];

  /**
   * An uri with a placeholder domain, to use in requests by default.
   *
   * It is used to produce absolute links when no request is pushed manually to
   * the 'request_stack' service.
   *
   * You can override this default behavior in your unit test via:
   * ```
   * TestHelpers::service('request_stack')->push(Request::create('https://example.com/some-path');
   * ```
   *
   * Example of how to check the url with the default request stub:
   * ```
   * $this->assertEquals(
   *   TestHelpers::REQUEST_STUB_DEFAULT_URI,
   *   $service->getCurrentRequest()->getUri()
   * );
   * ```
   *
   * @var string
   */
  public const REQUEST_STUB_DEFAULT_URI = 'http://drupal-unit-test.local/';

  /**
   * Gets a private or protected method from a class using reflection.
   *
   * @param object|string $class
   *   The class instance or the name of the class.
   * @param string $methodName
   *   The name of the method to get.
   *
   * @return \ReflectionMethod
   *   The method instance.
   */
  public static function getPrivateMethod($class, string $methodName): \ReflectionMethod {
    $reflection = new \ReflectionClass($class);
    $method = $reflection
      ->getMethod($methodName);
    $method
      ->setAccessible(TRUE);
    return $method;
  }

  /**
   * Calls a private or protected method from a class using reflection.
   *
   * @param object|string $class
   *   The class instance or the name of the class.
   * @param string $methodName
   *   The name of the method to get.
   * @param array $arguments
   *   The list of arguments for the calling method.
   *
   * @return mixed
   *   The return value of the executed function.
   */
  public static function callPrivateMethod($class, string $methodName, array $arguments = []) {
    $method = self::getPrivateMethod($class, $methodName);
    return $method->invokeArgs(is_object($class) ? $class : NULL, $arguments);
  }

  /**
   * Gets a private or protected property from a class using reflection.
   *
   * @param object|string $class
   *   The class instance or the name of the class.
   * @param string $propertyName
   *   The name of the property to get.
   * @param bool $returnReflectionProperty
   *   Flag to return a ReflectionProperty object instead of value.
   *
   * @return mixed
   *   The property value.
   */
  public static function getPrivateProperty($class, string $propertyName, $returnReflectionProperty = FALSE) {
    $reflection = new \ReflectionClass($class);
    if (is_string($class)) {
      // Considering property as a static.
      return $reflection->getStaticPropertyValue($propertyName);
    }
    $property = $reflection
      ->getProperty($propertyName);
    $property
      ->setAccessible(TRUE);
    if ($returnReflectionProperty) {
      return $property;
    }
    return $property->getValue($class);
  }

  /**
   * Sets a private or protected property value in a class using reflection.
   *
   * @param object|string $class
   *   The class instance or the name of the class.
   * @param string $propertyName
   *   The name of the property to get.
   * @param mixed $value
   *   The value to set.
   */
  public static function setPrivateProperty($class, string $propertyName, $value): void {
    $reflection = new \ReflectionClass($class);
    $property = $reflection
      ->getProperty($propertyName);
    $property
      ->setAccessible(TRUE);
    if (is_object($class)) {
      $property->setValue($class, $value);
    }
    else {
      // For static not initialized classes.
      $property->setValue(NULL, $value);
    }
  }

  /**
   * Sets a closure function to a class method.
   *
   * This makes private class methods accessible inside the function via $this.
   *
   * @param object $class
   *   The mocked class.
   * @param string $method
   *   The method name.
   * @param \Closure $closure
   *   The closure function to bind.
   */
  public static function setMockedClassMethod(object $class, string $method, \Closure $closure): void {
    $doClosure = $closure->bindTo($class, get_class($class));
    $class->method($method)->willReturnCallback($doClosure);
  }

  /**
   * Gets a mocked method from the Mock object to replace return value.
   *
   * This allows to replace the return value of the already defined method via
   * `$mockedMethod->willReturn('New Value')`.
   *
   * It's not possible with PHPUnit API, but here is a feature request about it:
   * https://github.com/sebastianbergmann/phpunit/issues/5070 - vote!
   *
   * @param \PHPUnit\Framework\MockObject\MockObject $mock
   *   A mocked object.
   * @param string $method
   *   A method to get.
   *
   * @return \PHPUnit\Framework\MockObject\Builder\InvocationMocker
   *   An InvocationMocker object with the method.
   */
  public static function getMockedMethod(MockObject $mock, string $method) {
    $invocationHandler = $mock->__phpunit_getInvocationHandler();
    $configurableMethods = self::getPrivateProperty($invocationHandler, 'configurableMethods');
    $matchers = self::getPrivateProperty($invocationHandler, 'matchers');
    foreach ($matchers as $matcher) {
      $methodNameRuleObject = self::getPrivateProperty($matcher, 'methodNameRule');
      if ($methodNameRuleObject->matchesName($method)) {
        return new InvocationMocker(
          $invocationHandler,
          $matcher,
          ...$configurableMethods
        );
      }
    }
    throw new MethodNameNotConfiguredException();
  }

  /**
   * Gets a path to file for the class.
   *
   * @param mixed $class
   *   Class name or path.
   *
   * @return bool|string
   *   The path to the file.
   */
  public static function getClassFile($class) {
    $reflection = new \ReflectionClass($class);
    return $reflection->getFileName();
  }

  /**
   * Finds a Drupal root directory.
   *
   * @return string
   *   A path to the Drupal root directory.
   */
  public static function getDrupalRoot(): string {
    static $path;
    if (!$path) {
      if (class_exists('\Drupal')) {
        $rc = new \ReflectionClass(\Drupal::class);
        $drupalClassAbsolutePath = $rc->getFileName();
        $drupalClassRelativePath = 'core/lib/Drupal.php';
        $path = substr($drupalClassAbsolutePath, 0, -strlen($drupalClassRelativePath) - 1);
      }
      else {
        $path = __DIR__;
        while (!file_exists($path . '/core/lib/Drupal.php')) {
          $path = dirname($path);
          if ($path == '') {
            throw new \Exception('Drupal root directory cannot be found.');
          }
        }
      }
    }
    return $path;
  }

  /**
   * Asserts that a function throws a specific exception.
   *
   * @param callable $function
   *   A function to execute.
   * @param string $exceptionClass
   *   (optional) An exception class to assert, \Exception by default.
   * @param string $message
   *   (optional) A message text to throw on missing exception.
   *
   * @todo Cover this function by a unit test.
   */
  public static function assertException(callable $function, ?string $exceptionClass = NULL, ?string $message = NULL) {
    $exceptionClass ??= '\Exception';
    $message ??= "An exception instance of $exceptionClass is expected.";
    try {
      $function();
      Assert::fail($message);
    }
    catch (\Throwable $e) {
      if (
        !$e instanceof AssertionFailedError
        && $e instanceof $exceptionClass
      ) {
        Assert::assertInstanceOf($exceptionClass, $e);
        return;
      }
      throw $e;
    }
  }

  /**
   * Parses the annotation for a class and gets the definition.
   *
   * @param string $class
   *   A class name to get definition.
   * @param string $plugin
   *   A plugin id.
   * @param string $annotationName
   *   The name of an annotation to use.
   *
   * @return array|object|false
   *   The definition from the plugin, or FALSE if no definition is found.
   */
  public static function getPluginDefinition(string $class, string $plugin = 'TypedData', ?string $annotationName = NULL) {
    $rc = new \ReflectionClass($class);

    // The plugin definition can be in PHP attributes or in annotations.
    // Try to read PHP attributes at first.
    $attributes = $rc->getAttributes();
    if ($attributes[0] ?? FALSE) {
      $attribute = $attributes[0]->newInstance();
      static $attributeClassDiscovery;
      $attributeClassDiscovery ??= new AttributeClassDiscovery('', new \ArrayObject([]));
      // Calling a private method to enhance the annotation definition by the
      // class and the provider.
      // A copy of the code from the
      // \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery::parseClass()
      // @todo Rework without calling a private method.
      TestHelpers::callPrivateMethod(
        $attributeClassDiscovery,
        'prepareAttributeDefinition',
        [$attribute, $class]
      );

      $definition = $attribute->get();
      if (is_array($definition)) {
        $definitionClass = $definition['class'];
      }
      else {
        $definitionClass = get_class($definition);
      }

      // The annotation name can be different from the class name.
      // Also, we have different namespaces for definitions using
      // annotations and attributes, example:
      // \Drupal\Core\Entity\Annotation\ContentEntityType
      // \Drupal\Core\Entity\Attribute\ContentEntityType
      // So, checking the last part of the FQCN only.
      if ($annotationName) {
        $annotationNameParts = explode('\\', $annotationName);
        $annotationNameShort = array_pop($annotationNameParts);
        $definitionClassParts = explode('\\', $definitionClass);
        $definitionClassShort = array_pop($definitionClassParts);
        if ($definitionClassShort !== $annotationNameShort) {
          return FALSE;
        }
      }
      return $definition;
    }

    // Falling back to read annotations from comments.
    $reader = new SimpleAnnotationReader();
    $reader->addNamespace('Drupal\Core\Annotation');
    $reader->addNamespace('Drupal\Core\\' . $plugin . '\Annotation');
    // If no annotation name is passed, just getting the first annotation.
    if (!$annotationName) {
      $annotation = current($reader->getClassAnnotations($rc));
    }
    else {
      $annotation = $reader->getClassAnnotation($rc, $annotationName);
    }
    if ($annotation) {
      static $annotatedClassDiscovery;
      $annotatedClassDiscovery ??= new AnnotatedClassDiscovery('', new \ArrayObject([]));
      // Calling a private method to enhance the annotation definition by the
      // class and the provider.
      // @todo Rework without calling a private method.
      TestHelpers::callPrivateMethod(
        $annotatedClassDiscovery,
        'prepareAnnotationDefinition',
        [$annotation, $class]
      );
      $definition = $annotation->get();
      return $definition;
    }
    return FALSE;
  }

  /**
   * Creates a class via calling function create() with container.
   *
   * @param string|object $class
   *   The class to test, can be a string with path or initialized class.
   * @param array $createArguments
   *   The list of arguments for passing to the function create(), excluding
   *   the container as the first argument, because it is mandatory, so it is
   *   passed automatically.
   * @param array $services
   *   The array of services to add to the container.
   *   Format is same as in function setServices().
   *
   * @return object
   *   The initialized class instance.
   */
  public static function createClass($class, ?array $createArguments = NULL, ?array $services = NULL): object {
    if ($services) {
      self::setServices($services);
    }
    $container = self::getContainer();
    $createArguments ??= [];
    $classInstance = $class::create($container, ...$createArguments);
    return $classInstance;
  }

  /**
   * Initializes a service from YAML file with passing services as arguments.
   *
   * @param string|array $servicesYamlFileOrData
   *   The path to the YAML file, or an array with data from YAML.
   * @param string $name
   *   The name of the service.
   * @param array|null $mockMethods
   *   A list of method to mock when creating the instance.
   * @param string|null $overrideClass
   *   A class to override the default service class.
   * @param array|null $customArguments
   *   An array of arguments to pass to the service constructor, overriding
   *   the default values from the YAML file.
   *
   * @return object
   *   The initialized class instance.
   */
  public static function initServiceFromYaml(
    $servicesYamlFileOrData,
    string $name,
    ?array $mockMethods = NULL,
    ?string $overrideClass = NULL,
    ?array $customArguments = NULL,
  ): object {
    if (is_string($servicesYamlFileOrData)) {
      $serviceInfo = self::getServiceInfoFromYaml($name, $servicesYamlFileOrData);
      $serviceInfo['class'] ??= $name;
    }
    elseif (is_array($servicesYamlFileOrData)) {
      $serviceInfo = $servicesYamlFileOrData;
    }
    else {
      throw new \Error('The first argument should be a path to a YAML file, or array with data.');
    }
    return self::initServiceFromInfo($serviceInfo, $mockMethods, $customArguments);
  }

  /**
   * Replaces parameters and services to real values in service arguments.
   *
   * @param array $arguments
   *   A list of raw arguments.
   *
   * @return array
   *   A list of resolved arguments.
   *
   * @internal For internal usage only.
   */
  private static function resolveServiceArguments(array $arguments = []): array {
    $container = self::getContainer();
    // If we have no default parameters in the container, fill them.
    if (!$container->hasParameter('app.root')) {
      if (defined('TEST_HELPERS_DRUPAL_CORE_PARAMETERS')) {
        // Use prefilled values to speed up the parsing.
        self::loadParametersFromJson(TEST_HELPERS_DRUPAL_CORE_PARAMETERS);
      }
      else {
        self::loadParametersFromYamlFile(self::getDrupalRoot() . DIRECTORY_SEPARATOR . 'core/core.services.yml');
      }
    }
    if (
      !$container->hasParameter('app.root')
      || $container->getParameter('app.root') == ''
    ) {
      $container->setParameter('app.root', self::getDrupalRoot());
    }

    // These parameters are set on the runtime by the function
    // ListCacheBinsPass::process().
    if (!$container->hasParameter('cache_bins')) {
      $container->setParameter('cache_bins', []);
    }
    if (!$container->hasParameter('cache_default_bin_backends')) {
      $container->setParameter('cache_default_bin_backends', []);
    }
    if (!$container->hasParameter('memory_cache_bins')) {
      $container->setParameter('memory_cache_bins', []);
    }
    // The `memory_cache_default_bin_backends` is required to init some
    // services, but missing in the `core.services.yml`, so setting it manually.
    if (!$container->hasParameter('memory_cache_default_bin_backends')) {
      $container->setParameter('memory_cache_default_bin_backends', []);
    }

    // The `hook_implementations_map` is set on the runtime by the
    // function HookCollectorPass::writeImplementationsToContainer().
    if (!$container->hasParameter('hook_implementations_map')) {
      $container->setParameter('hook_implementations_map', []);
    }

    // The `entity.memory_cache.slots` should be present from the core
    // services map, but in some cases it is missing. Force it to be set.
    if (!$container->hasParameter('entity.memory_cache.slots')) {
      $container->setParameter('entity.memory_cache.slots', 1000);
    }
    $classArguments = [];
    foreach ($arguments as $argumentKey => $argument) {
      // If the name starts with `$`, it's a named argument for a function,
      // that should come without the `$` prefix.
      if (str_starts_with($argumentKey, '$')) {
        $argumentKey = substr($argumentKey, 1);
      }
      if (!is_string($argument)) {
        $classArguments[$argumentKey] = $argument;
        continue;
      }
      $firstCharacter = substr($argument, 0, 1);
      if (substr($argument, 1, 1) == '?') {
        $argument = str_replace('?', '', $argument);
      }
      if ($firstCharacter == '@') {
        $classArguments[$argumentKey] = self::service(substr($argument, 1));
      }
      elseif ($firstCharacter == '%') {
        $key = trim($argument, '%');
        if ($container->hasParameter($key)) {
          $resolved = $container->getParameter($key);
        }
        else {
          switch ($key) {
            case 'language.default_values':
              $resolved = Language::$defaultValues;
              $resolved['label'] = $resolved['name'];
              break;

            case 'cache_contexts':
              $resolved = [];
              break;

            case 'container.modules':
              $resolved = [];
              break;

            default:
              throw new \Error("Container parameter '$key' is missing.\nAdd it using TestHelpers::getContainer()->setParameter('$key', \$value);");
          }
        }
        $classArguments[$argumentKey] = $resolved;
      }
      else {
        $classArguments[$argumentKey] = $argument;
      }
    }
    return $classArguments;
  }

  /**
   * Initializes a service from the service name or class.
   *
   * The function tries to auto detect the service YAML file location
   * automatically by service name or class name.  If auto magic doesn't work
   * for your case, use the initServiceFromYaml() directly.
   *
   * @param string $serviceNameOrClass
   *   The name of the service id in YAML file of the current module,
   *   or the full name of a service class.
   * @param string $serviceNameToCheck
   *   A service name to check matching the declared one in services.yml file.
   *   Acts only if the class name is passed as a first argument.
   * @param array|null $mockMethods
   *   A list of method to mock when creating the instance.
   * @param string|null $overrideClass
   *   A class to override the default service class.
   * @param array|null $customArguments
   *   An array of arguments to pass to the service constructor, overriding
   *   the default values from the YAML file.
   *
   * @return object
   *   The initialized class instance.
   */
  public static function initService(
    string $serviceNameOrClass,
    ?string $serviceNameToCheck = NULL,
    ?array $mockMethods = NULL,
    ?string $overrideClass = NULL,
    ?array $customArguments = NULL,
  ): object {
    // If we have just a service name, not a class.
    if (strpos($serviceNameOrClass, '\\') === FALSE) {
      $serviceName = $serviceNameOrClass;
      self::requireCoreFeaturesMap();
      if (isset(TEST_HELPERS_DRUPAL_CORE_SERVICE_MAP[$serviceName])) {
        return self::initServiceFromYaml(TEST_HELPERS_DRUPAL_CORE_SERVICE_MAP[$serviceName], $serviceName, $mockMethods, $overrideClass);
      }
      else {
        // We have a service id name, use the current module as the module name.
        $callerInfo = self::getCallerInfo();
        $moduleName = self::getModuleName($callerInfo['class']);
        $moduleRoot = self::getModuleRoot($callerInfo['file'], $moduleName);
        $servicesFile = "$moduleRoot/$moduleName.services.yml";
        return self::initServiceFromYaml($servicesFile, $serviceName, $mockMethods, $overrideClass);
      }
    }
    else {
      $serviceClass = ltrim($serviceNameOrClass, '\\');
      $serviceInfo = self::getServiceInfoFromClass($serviceClass);
      if (!$serviceInfo) {
        throw new \Exception("Can't find the service name by class $serviceClass.");
      }
      $serviceName = $serviceInfo['#name'];
      $servicesFile = $serviceInfo['#file'];
      if ($serviceNameToCheck && $serviceNameToCheck !== $serviceName) {
        throw new \Exception("The service name '$serviceName' differs from required name '$serviceNameToCheck'");
      }
      return self::initServiceFromYaml($servicesFile, $serviceName, $mockMethods, $overrideClass);
    }
  }

  /**
   * Gets information about service from YAML file.
   *
   * @param string $serviceClass
   *   A class to search.
   *
   * @return array|null
   *   An array with information, or NULL if nothing found.
   */
  public static function getServiceInfoFromClass(string $serviceClass): ?array {
    $serviceClass = ltrim($serviceClass, '\\');
    $moduleName = self::getModuleName($serviceClass);
    $reflection = new \ReflectionClass($serviceClass);
    $fileName = $reflection->getFileName();
    $moduleRoot = self::getModuleRoot($fileName, $moduleName);
    $servicesFile = "$moduleRoot/$moduleName.services.yml";
    try {
      $servicesFileData = self::parseYamlFile($servicesFile);
    }
    catch (\Exception) {
      return NULL;
    }
    foreach ($servicesFileData['services'] ?? [] as $name => $info) {
      if (isset($info['class'])) {
        $checkingClass = ltrim($info['class'], '\\');
      }
      else {
        $checkingClass = ltrim($name, '\\');
      }
      if ($checkingClass == $serviceClass) {
        $info['class'] = $checkingClass;
        $info['#name'] = $name;
        $info['#file'] = $servicesFile;
        return $info;
      }
    }
    return NULL;
  }

  /**
   * Gets a Drupal services container, or creates a new one.
   *
   * @param bool $forceCreate
   *   Force create a new container, even if already exists.
   *
   * @return \Symfony\Component\DependencyInjection\ContainerInterface
   *   The initialized container.
   */
  public static function getContainer($forceCreate = FALSE): ContainerInterface {
    if ($forceCreate || !\Drupal::hasContainer()) {
      $container = new ContainerBuilder();
      // Setting default parameters, required for some Core services.
      $container->setParameter('cache_bins', []);
      $container->set('kernel', new DrupalKernelStub());
      \Drupal::setContainer($container);
    }
    return \Drupal::getContainer();
  }

  /**
   * Gets the service stub or mock, or initiates a new one if missing.
   *
   * @param string $serviceName
   *   The service name.
   * @param object|string|null $class
   *   The class to use in service, allowed different types:
   *   - object: attaches the initialized object to the service.
   *   - string: creates a mock of the class by passed name.
   *   - null: use stub from Test Helpers of default class from Drupal Core.
   * @param bool $forceOverride
   *   Control overriding the service:
   *   - FALSE on NULL: overrides only if the class names are different.
   *   - TRUE: always overrides the class by a new instance.
   * @param array $mockMethods
   *   The list of exist methods to make mockable.
   * @param array $addMockableMethods
   *   The list of new methods to make them mockable.
   * @param bool $initService
   *   Initializes core service with constructor and passing all dependencies.
   * @param string $servicesYamlFile
   *   A path to the services.yaml file when it can't be properly autodetect.
   * @param array|null $customArguments
   *   An array of arguments to pass to the service constructor, overriding
   *   the default values from the YAML file.
   *
   * @return object
   *   The initialized service object.
   */
  public static function service(
    string $serviceName,
    $class = NULL,
    ?bool $forceOverride = NULL,
    ?array $mockMethods = NULL,
    ?array $addMockableMethods = NULL,
    ?bool $initService = NULL,
    ?string $servicesYamlFile = NULL,
    ?array $customArguments = NULL,
  ): object {
    $addMockableMethods ??= [];
    $container = self::getContainer();
    if ($container->has($serviceName) && $class === NULL && !$forceOverride) {
      return $container->get($serviceName);
    }

    // Only use stubs if the class is not explicitly set.
    if ($class == NULL) {
      $serviceStub = self::SERVICES_CUSTOM_STUBS[$serviceName] ?? NULL;
    }
    else {
      $serviceStub = NULL;
    }

    if ($initService === NULL) {
      if (
        in_array($serviceName, self::SERVICES_CORE_INIT)
        || $serviceStub
      ) {
        $initService = TRUE;
      }
      else {
        $initService = FALSE;
      }
    }

    if (is_object($class)) {
      $service = $class;
    }
    elseif (is_string($class)) {
      if ($initService) {
        $service = self::initService($class, NULL, $mockMethods, $customArguments);
      }
      else {
        // @todo Add $addMockableMethods.
        $service = self::createMock($class);
      }
    }
    elseif ($class === NULL) {
      $serviceInfo = self::getServiceInfo($serviceName, $servicesYamlFile);

      if ($initService) {
        if (is_array($serviceStub)) {
          $service = call_user_func_array($serviceStub, []);
        }
        else {
          if (isset($serviceStub)) {
            $serviceInfo['class'] = $serviceStub;
          }
          $service = self::initServiceFromInfo($serviceInfo, $mockMethods, $customArguments);
        }
      }
      else {
        $service = self::createMock($serviceInfo['class']);
      }
    }
    else {
      throw new \Exception("Class should be an object, string as path to class, or NULL.");
    }

    if ($container->has($serviceName)) {
      $configuredService = $container->get($serviceName);

      // Checking if service of already defined and has the same class as
      // the passed service.
      if (
        (get_class($configuredService) !== get_class($service))
        || $forceOverride
      ) {
        $container->set($serviceName, $service);
        return $service;
      }

      return $configuredService;
    }
    else {
      $container->set($serviceName, $service);
      return $service;
    }
  }

  /**
   * Initializes a service from the services info array from YAML file.
   *
   * @param array $info
   *   An array with service information, like in services YAML file.
   * @param array $mockMethods
   *   A list of methods to mock.
   * @param array|null $customArguments
   *   An array of arguments to pass to the service constructor, overriding
   *   the default values from the YAML file.
   *
   * @return object
   *   The service instance.
   *
   * @internal For internal usage only.
   */
  private static function initServiceFromInfo(array $info, ?array $mockMethods = NULL, ?array $customArguments = NULL) {
    if ($customArguments) {
      $info['arguments'] = $customArguments;
    }
    $info['arguments'] ??= [];
    if (isset($info['arguments'])) {
      $info['arguments'] = self::resolveServiceArguments($info['arguments']);
    }
    if ($mockMethods) {
      $service = TestHelpers::createPartialMockWithConstructor(
        $info['class'],
        $mockMethods,
        $info['arguments'],
      );
    }
    else {
      if (isset($info['factory'])) {
        if (is_string($info['factory'])) {
          $service = new $info['class'](...$info['arguments']);
        }
        elseif (is_array($info['factory'])) {
          $factoryParams = $info['factory'];
          $factoryClass = array_shift($factoryParams);
          if (str_starts_with($factoryClass, '@')) {
            $factory = self::service(substr($factoryClass, 1), NULL, NULL, NULL, NULL, TRUE);
          }
          else {
            $factory = new $factoryClass();
          }
          $factoryMethod = array_shift($factoryParams);
          $arguments = self::resolveServiceArguments($info['arguments']);
          $service = $factory->$factoryMethod(...$arguments);
        }
      }
      else {
        $service = new $info['class'](...$info['arguments']);
      }
    }
    // @todo Make a better implementation of this check.
    if (
      // @phpstan-ignore-next-line The $service is always defined.
      method_exists($service, 'setContainer')
      // The `setContainer()` is deprecated for some services in Drupal 10.3.x.
      && !$service instanceof LoggerChannelFactory
      && !$service instanceof EntityTypeManager
    ) {
      $service->setContainer(self::getContainer());
    }
    // @phpstan-ignore-next-line The $service is always defined.
    return $service;
  }

  /**
   * Initializes list of services and adds them to the container.
   *
   * @param array $services
   *   An array with services, supports two formats:
   *   - A numeric array with service names: adds default classes.
   *   - An associative array with service name as a key and object or NULL
   *     in value: Attaches the passed class to the service, if NULL - creates
   *     a stub for default Drupal Core class.
   * @param bool $clearContainer
   *   Clears the Drupal container, if TRUE.
   * @param bool $forceOverride
   *   Control overriding the service:
   *   - FALSE on NULL: overrides only if the class names are different.
   *   - TRUE: always overrides the class by a new instance.
   * @param bool $initServices
   *   Initializes core service with constructor and passing all dependencies.
   */
  public static function setServices(
    array $services,
    ?bool $clearContainer = NULL,
    ?bool $forceOverride = NULL,
    ?bool $initServices = NULL,
  ): void {
    if ($clearContainer) {
      TestHelpers::getContainer(TRUE);
    }
    foreach ($services as $key => $value) {
      if (is_int($key)) {
        // If we have only a service name - just reuse the default behavior.
        self::service($value);
      }
      else {
        // If we have a service name in key and class in value - pass the class.
        self::service($key, $value);
      }
    }
  }

  /**
   * Creates a stub entity for an entity type from a given class.
   *
   * @param string $entityTypeNameOrClass
   *   A full path to an entity type class, or an entity type id for Drupal
   *   Core entities like `node`, `taxonomy_term`, etc.
   * @param array $values
   *   A list of values to set in the created entity.
   * @param array $translations
   *   A list of translations to add to the created entity.
   * @param array $options
   *   A list of options to entity stub creation:
   *   - mockMethods: list of methods to make mockable.
   *   - addMethods: list of additional methods.
   *   - skipPrePostSave: a flag to use direct save on the storage without
   *     calling preSave and postSave functions. Can be useful if that functions
   *     have dependencies which hard to mock. Applies only on the first
   *     initialization of this node type.
   *   - skipEntityConstructor: a flag to skip calling the entity constructor.
   *   - fields: a list of custom field options by field name.
   *     Applies only on the first initialization of this field.
   *     Supportable formats:
   *     - A string, indicating field type, like 'integer', 'string',
   *       'entity_reference', only core field types are supported.
   *     - An array with field configuration: type, settings, etc, like this:
   *       [
   *        'type' => 'entity_reference',
   *        'settings' => ['target_type' => 'node']
   *        'translatable' => TRUE,
   *        'required' => FALSE,
   *        'cardinality' => 3,
   *       ].
   *     - A field definition object, that will be applied to the field.
   *
   * @return \Drupal\test_helpers\Stub\EntityStubInterface|\Drupal\Core\Entity\EntityInterface|\PHPUnit\Framework\MockObject\MockObject
   *   The stub object for the entity.
   */
  public static function createEntity(string $entityTypeNameOrClass, ?array $values = NULL, ?array $translations = NULL, ?array $options = NULL) {
    $options ??= [];
    // Splitting $options to entity options and storage options.
    if (isset($options['skipPrePostSave'])) {
      $storageOptions['skipPrePostSave'] = $options['skipPrePostSave'];
      unset($options['skipPrePostSave']);
    }
    return EntityStubFactory::create($entityTypeNameOrClass, $values, $translations, $options, $storageOptions ?? NULL);
  }

  /**
   * Creates a stub entity for an entity type from a given class and saves it.
   *
   * @param string $entityTypeNameOrClass
   *   A full path to an entity type class, or an entity type id for Drupal
   *   Core entities like `node`, `taxonomy_term`, etc.
   * @param array $values
   *   A list of values to set in the created entity.
   * @param array $translations
   *   A list of translations to add to the created entity.
   * @param array $options
   *   A list of options to entity stub creation:
   *   - mockMethods: list of methods to make mockable.
   *   - addMethods: list of additional methods.
   *   - skipPrePostSave: a flag to use direct save on the storage without
   *     calling preSave and postSave functions. Can be useful if that functions
   *     have dependencies which hard to mock. Applies only on the first
   *     initialization of this node type.
   *   - skipEntityConstructor: a flag to skip calling the entity constructor.
   *   - fields: a list of custom field options by field name.
   *     Applies only on the first initialization of this field.
   *     Supportable formats:
   *     - A string, indicating field type, like 'integer', 'string',
   *       'entity_reference', only core field types are supported.
   *     - An array with field configuration: type, settings, etc, like this:
   *       [
   *        'type' => 'entity_reference',
   *        'settings' => ['target_type' => 'node']
   *        'translatable' => TRUE,
   *        'required' => FALSE,
   *        'cardinality' => 3,
   *       ].
   *     - A field definition object, that will be applied to the field.
   *
   * @return \Drupal\test_helpers\Stub\EntityStubInterface|\Drupal\Core\Entity\EntityInterface|\PHPUnit\Framework\MockObject\MockObject
   *   The stub object for the entity.
   */
  public static function saveEntity(string $entityTypeNameOrClass, ?array $values = NULL, ?array $translations = NULL, ?array $options = NULL) {
    $entity = self::createEntity($entityTypeNameOrClass, $values, $translations, $options);
    $entity->save();
    return $entity;
  }

  /**
   * Gets or initializes an Entity Storage for a given Entity Type class name.
   *
   * @param string $entityTypeNameOrClass
   *   The entity class.
   * @param \Drupal\Core\Entity\EntityStorageInterface $storageInstance
   *   An already initialized instance of a storage, NULL to create a new one.
   * @param bool|null $forceOverride
   *   Forces creation of the new clear storage, if exists.
   * @param array $storageOptions
   *   A list of options to pass to the storage initialization. Acts only once
   *   if the storage is not initialized yet.
   *   - skipPrePostSave: a flag to use direct save on the storage without
   *     calling preSave and postSave functions. Can be useful if that functions
   *     have dependencies which hard to mock.
   *   - constructorArguments: additional arguments to the constructor.
   *   - mockMethods: a list of storage methods to mock.
   *   - addMethods: a list of storage methods to add.
   *
   * @return \Drupal\Core\Entity\EntityStorageInterface
   *   The initialized stub of Entity Storage.
   */
  public static function getEntityStorage(string $entityTypeNameOrClass, ?EntityStorageInterface $storageInstance = NULL, ?bool $forceOverride = NULL, ?array $storageOptions = NULL): EntityStorageInterface {
    return self::service('entity_type.manager')->stubGetOrCreateStorage($entityTypeNameOrClass, $storageInstance, $forceOverride, $storageOptions);
  }

  /**
   * Creates a field instance stub.
   *
   * @param array|string|null $values
   *   The field values.
   * @param string|\Drupal\Core\Field\FieldDefinitionInterface|null $typeOrDefinition
   *   A field type like 'string', 'integer', 'boolean'.
   *   Or a path to a field class like
   *   Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem.
   *   Or a ready definition object to use.
   *   If null - will be created a stub with fallback ItemStubItem definition.
   * @param string|null $name
   *   The field name.
   * @param \Drupal\Core\TypedData\TypedDataInterface|null $parent
   *   Parent item for attaching to the field.
   * @param bool|null $isBaseField
   *   A flag to create a base field instance.
   * @param array|null $mockMethods
   *   A list of method to mock when creating the instance.
   *
   * @return \Drupal\Core\Field\FieldItemListInterface
   *   A field item list with items as stubs.
   */
  public static function createFieldStub(
    $values = NULL,
    $typeOrDefinition = NULL,
    ?string $name = NULL,
    ?TypedDataInterface $parent = NULL,
    $isBaseField = NULL,
    ?array $mockMethods = NULL,
  ): FieldItemListInterface {
    return FieldItemListStubFactory::create($name, $values, $typeOrDefinition, $parent, $isBaseField, $mockMethods);
  }

  /**
   * Adds a field plugin from class to the typed data manager.
   *
   * @param string $class
   *   The field plugin class.
   */
  public static function addFieldPlugin(string $class): void {
    TestHelpers::service('typed_data_manager')->stubAddFieldType($class);
  }

  /**
   * Initializes the main services to work with entities stubs.
   *
   * Initializes a bundle of services, required to work with entity stubs:
   * - entity_type.manager
   * - language_manager
   * - entity_field.manager
   * - entity.query.sql
   * - string_translation
   * - plugin.manager.field.field_type
   * - typed_data_manager
   * - uuid
   * Also adds them to the Drupal Container.
   */
  public static function initEntityTypeManagerStubs(): void {
    self::service('entity_type.manager');
  }

  /* ************************************************************************ *
   * Helpers for queries.
   * ************************************************************************ */

  /**
   * Performs matching of passed conditions with the query.
   *
   * @param \Drupal\Core\Entity\Query\ConditionInterface|\Drupal\Core\Database\Query\ConditionInterface $query
   *   The query object to check.
   * @param \Drupal\Core\Entity\Query\ConditionInterface|\Drupal\Core\Database\Query\ConditionInterface $queryExpected
   *   The query object with expected conditions.
   * @param bool $onlyListed
   *   Forces to return false, if the checking query object contains more
   *   conditions than in object with expected conditions.
   * @param bool $throwErrors
   *   Enables throwing notice errors when matching fails, with the explanation
   *   what exactly doesn't match.
   *
   * @return bool
   *   True if is subset, false if not.
   */
  public static function queryIsSubsetOf(object $query, object $queryExpected, bool $onlyListed = FALSE, bool $throwErrors = TRUE): bool {
    if ($query instanceof DatabaseSelectInterface && $queryExpected instanceof DatabaseSelectInterface) {
      $order = self::getPrivateProperty($query, 'order');
      $orderExpected = self::getPrivateProperty($queryExpected, 'order');
      if (!self::isNestedArraySubsetOf($order, $orderExpected)) {
        $throwErrors && self::throwMatchError('order', $orderExpected, $order);
        return FALSE;
      }

    }
    elseif ($query instanceof EntityQueryInterface && $queryExpected instanceof EntityQueryInterface) {
      if ($query->getEntityTypeId() != $queryExpected->getEntityTypeId()) {
        $throwErrors && self::throwMatchError('entity type', $queryExpected->getEntityTypeId(), $query->getEntityTypeId());
        return FALSE;
      }
      $sort = self::getPrivateProperty($query, 'sort');
      $sortExpected = self::getPrivateProperty($queryExpected, 'sort');
      if (!self::isNestedArraySubsetOf($sort, $sortExpected)) {
        $throwErrors && self::throwMatchError('sort', $sortExpected, $sort);
        return FALSE;
      }
      if (is_bool($accessCheckExpected = self::getPrivateProperty($queryExpected, 'accessCheck'))) {
        if ($accessCheckExpected !== $accessCheckActual = self::getPrivateProperty($query, 'accessCheck')) {
          $throwErrors && self::throwMatchError('accessCheck', $accessCheckExpected, $accessCheckActual);
          return FALSE;
        }
      }

    }
    else {
      throw new \Exception('Unsupportable query types.');
    }
    $range = self::getPrivateProperty($query, 'range');
    $rangeExpected = self::getPrivateProperty($queryExpected, 'range');
    if (!self::isNestedArraySubsetOf($range, $rangeExpected)) {
      $throwErrors && self::throwMatchError('range', $rangeExpected, $range);
      return FALSE;
    }

    $conditions = self::getPrivateProperty($query, 'condition');
    $conditionsExpected = self::getPrivateProperty($queryExpected, 'condition');
    if (!self::matchConditions($conditions, $conditionsExpected, $onlyListed, $throwErrors)) {
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Searches query conditions by a field name or sub-conditions.
   *
   * @param object $query
   *   The query object.
   * @param string|array $requiredCondition
   *   The string with the field name to search.
   *   Or an array with sub-conditions like
   *   `['field' => 'nid', 'operator' => '<>']`.
   * @param bool $returnAllMatches
   *   A flag to return all matches as a list, not only the first match.
   *
   * @return array|null
   *   The first matched condition, or NULL if no matches.
   */
  public static function findQueryCondition(object $query, $requiredCondition, bool $returnAllMatches = FALSE): ?array {
    $conditionsProperty = self::getPrivateProperty($query, 'condition');
    $conditions = $conditionsProperty->conditions();
    $matches = [];
    foreach ($conditions as $condition) {
      if (
        (is_string($requiredCondition) && ($condition['field'] ?? NULL) == $requiredCondition)
        || (is_array($requiredCondition) && self::isNestedArraySubsetOf($condition, $requiredCondition))
      ) {
        if ($returnAllMatches) {
          $matches[] = $condition;
        }
        else {
          return $condition;
        }
      }
    }
    if ($returnAllMatches && $matches) {
      return $matches;
    }
    return NULL;
  }

  /**
   * Performs matching of passed conditions with the query.
   *
   * @param \Drupal\Core\Entity\Query\ConditionInterface|\Drupal\Core\Database\Query\ConditionInterface $conditionsObject
   *   The query object to check.
   * @param \Drupal\Core\Entity\Query\ConditionInterface|\Drupal\Core\Database\Query\ConditionInterface $conditionsExpectedObject
   *   The query object with expected conditions.
   * @param bool|null $onlyListed
   *   Forces to return false, if the checking query object contains more
   *   conditions than in object with expected conditions.
   * @param bool|null $throwErrors
   *   Enables throwing notice errors when matching fails, with the explanation
   *   what exactly doesn't match.
   *
   * @return bool
   *   True if is subset, false if not.
   */
  public static function matchConditions(object $conditionsObject, object $conditionsExpectedObject, ?bool $onlyListed = NULL, bool $throwErrors = FALSE): bool {
    if ($conditionsObject instanceof EntityQueryConditionInterface) {
      if (strcasecmp($conditionsObject->getConjunction(), $conditionsExpectedObject->getConjunction()) != 0) {
        $throwErrors && self::throwMatchError('conjunction', $conditionsObject->getConjunction(), $conditionsExpectedObject->getConjunction());
        return FALSE;
      }
      $conditions = $conditionsObject->conditions();
      $conditionsExpected = $conditionsExpectedObject->conditions();
    }
    elseif ($conditionsObject instanceof DatabaseQueryConditionInterface) {
      if (strcasecmp($conditionsObject->conditions()['#conjunction'], $conditionsExpectedObject->conditions()['#conjunction']) != 0) {
        $throwErrors && self::throwMatchError('conjunction', $conditionsExpectedObject->conditions()['#conjunction'], $conditionsObject->conditions()['#conjunction']);
        return FALSE;
      }
      $conditions = $conditionsObject->conditions();
      unset($conditions['#conjunction']);
      $conditionsExpected = $conditionsExpectedObject->conditions();
      unset($conditionsExpected['#conjunction']);
    }
    elseif (in_array('Drupal\search_api\Query\ConditionGroupInterface', class_implements($conditionsObject))) {
      if (strcasecmp($conditionsObject->getConjunction(), $conditionsExpectedObject->getConjunction()) != 0) {
        $throwErrors && self::throwMatchError('conjunction', $conditionsExpectedObject->getConjunction(), $conditionsObject->getConjunction());
        return FALSE;
      }
      $conditions = self::conditionsSearchApiObjectsToArray(self::getPrivateProperty($conditionsObject, 'conditions'));
      $conditionsExpected = self::conditionsSearchApiObjectsToArray(self::getPrivateProperty($conditionsExpectedObject, 'conditions'));
    }
    else {
      throw new \Exception("Conditions should implement Drupal\Core\Entity\Query\ConditionInterface or Drupal\Core\Database\Query\ConditionInterface.");
    }
    $conditionsFound = [];
    $conditionsExpectedFound = [];
    foreach ($conditions as $conditionDelta => $condition) {
      foreach ($conditionsExpected as $conditionsExpectedDelta => $conditionExpected) {
        if (is_object($condition['field']) || is_object($conditionExpected['field'])) {
          if (!is_object($condition['field']) || !is_object($conditionExpected['field'])) {
            continue;
          }
          $conditionGroupMatchResult = self::matchConditions($condition['field'], $conditionExpected['field'], $onlyListed, FALSE);
          if ($conditionGroupMatchResult === TRUE) {
            $conditionsExpectedFound[$conditionsExpectedDelta] = TRUE;
            $conditionsFound[$conditionDelta] = TRUE;
            break;
          }
        }
        elseif ($condition == $conditionExpected) {
          if (is_array($condition['value'])) {
            if ($condition['value'] == $conditionExpected['value']) {
              $conditionsExpectedFound[$conditionsExpectedDelta] = TRUE;
              $conditionsFound[$conditionDelta] = TRUE;
              break;
            }
          }
          else {
            $conditionsExpectedFound[$conditionsExpectedDelta] = TRUE;
            $conditionsFound[$conditionDelta] = TRUE;
            break;
          }
        }
      }
    }
    if (count($conditionsExpectedFound) != count($conditionsExpected)) {
      foreach ($conditionsExpected as $delta => $condition) {
        if (!isset($conditionsExpectedFound[$delta])) {
          // Happens when condition is a conditionGroup.
          if (is_object($condition['field'])) {
            $groupConditions = [];
            foreach ($condition['field']->conditions() as $groupCondition) {
              if (is_object($groupCondition['field'])) {
                // @todo Try to find the deep failing condition.
                $groupCondition['field'] = '[' . $groupCondition['field']->getConjunction() . 'ConditionGroup with ' . count($groupCondition['field']->conditions()) . ' items]';
              }
              $groupConditions[] = $groupCondition;
            }
            $throwErrors && self::throwUserError('The expected condition group "' . $condition['field']->getConjunction() . '" is not matching, items: ' . self::shorthandVarExport($groupConditions, TRUE));
            return FALSE;
          }
          $throwErrors && self::throwUserError('The expected condition is not found: ' . self::shorthandVarExport($condition, TRUE));
          return FALSE;
        }
      }
      $throwErrors && self::throwMatchError('count of matched conditions', count($conditionsExpected), count($conditionsExpectedFound));
      return FALSE;
    }
    if ($onlyListed && (count($conditions) != count($conditionsExpected))) {
      foreach ($conditions as $delta => $condition) {
        if (!isset($conditionsFound[$delta])) {
          $throwErrors && self::throwUserError('The condition is not listed in expected: ' . self::shorthandVarExport($condition, TRUE));
        }
      }
      $throwErrors && self::throwMatchError('count of conditions', count($conditions), count($conditionsExpectedFound));
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Matches a EntityQuery condition to entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to use.
   * @param array $condition
   *   The condition to check.
   *
   * @return bool
   *   True if matches, false if not.
   */
  public static function matchEntityCondition(EntityInterface $entity, array $condition): bool {
    $exceptionSuffix = ' Use function stubSetExecuteHandler() to stub the results.';
    if (strpos($condition['field'], '.')) {
      $parts = explode('.', $condition['field']);
      if (count($parts) > 2) {
        throw new \Error('Function does not support deep references in fields yet, only field property name is supported.' . $exceptionSuffix);
      }
      $fieldName = $parts[0];
      $propertyName = $parts[1];
    }
    else {
      $fieldName = $condition['field'];
    }
    $field = $entity->$fieldName;
    $fieldItem = $field[0] ?? NULL;
    if (!isset($propertyName)) {
      $propertyName = (is_object($fieldItem) && method_exists($fieldItem, 'mainPropertyName')) ? $fieldItem->mainPropertyName() : 'value';
    }
    $value = (is_object($field) && method_exists($field, 'getValue')) ? $field->getValue() : NULL;
    switch (is_string($condition['operator']) ? strtoupper($condition['operator']) : $condition['operator']) {
      case 'IN':
        if ($value == NULL && !empty($condition['value'])) {
          return FALSE;
        }
        foreach ($value as $valueItem) {
          if (!in_array($valueItem[$propertyName] ?? NULL, $condition['value'])) {
            return FALSE;
          }
        }
        return TRUE;

      case 'NOT IN':
        if ($value == NULL && !empty($condition['value'])) {
          return TRUE;
        }
        foreach ($value as $valueItem) {
          if (in_array($valueItem[$propertyName], $condition['value'])) {
            return FALSE;
          }
        }
        return TRUE;

      // NULL is treated as `=` condition for EntityQuery queries.
      case NULL:
      case '=':
        if (is_array($value)) {
          foreach ($value as $valueItem) {
            if (($valueItem[$propertyName] ?? NULL) == $condition['value']) {
              return TRUE;
            }
          }
        }
        return FALSE;

      case 'IS NULL':
        return empty($value);

      case 'IS NOT NULL':
        return !empty($value);

      case '<>':
      case '>':
      case '<':
      case '>=':
      case '<=':
        foreach ($value as $valueItem) {
          // To suppress `The use of function eval() is discouraged` warning.
          // @codingStandardsIgnoreStart
          if (eval ("return '" . addslashes($valueItem[$propertyName] ?? NULL) . "' " . $condition['operator'] . " '" . addslashes($condition['value']) . "';")) {
            // @codingStandardsIgnoreEnd
            return TRUE;
          }
        }
        return FALSE;

      default:
        throw new \Exception('A stub for the "' . $condition['operator'] . '" operator is not implemented yet.' . $exceptionSuffix);
    }
  }

  /**
   * Performs a check if the actual array is a subset of expected.
   *
   * @param mixed $array
   *   The array to check. Returns false if passed variable is not an array.
   * @param mixed $subset
   *   The array with values to check the subset.
   * @param bool $throwErrors
   *   Enables throwing notice errors when matching fails, with the explanation
   *   what exactly doesn't match.
   *
   * @return bool
   *   True if the array is the subset, false if not.
   */
  public static function isNestedArraySubsetOf($array, $subset, bool $throwErrors = FALSE): bool {
    static $throwErrorsStatic;

    $callbackFunction = self::class . '::isValueSubsetOfCallback';
    $callerInfo = self::getCallerInfo();
    if ($callerInfo['class'] . '::' . $callerInfo['function'] !== $callbackFunction) {
      $throwErrorsStatic = $throwErrors;
    }
    if ($subset === NULL) {
      return TRUE;
    }
    if (!is_array($array)) {
      $throwErrorsStatic && self::throwMatchError('array', $subset, $array);
      return FALSE;
    }
    if (!is_array($subset)) {
      $throwErrorsStatic && self::throwMatchError('subset', $subset, $array);
      return FALSE;
    }
    $result = array_uintersect_assoc($subset, $array, $callbackFunction);
    if ($result != $subset) {
      $throwErrorsStatic && self::throwMatchError('arrays', $subset, $array);
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Calls an event subscriber with checking the definition in services.
   *
   * Checks that the event subscriber has a definition in services.yml file
   * and the 'event_subscriber' tag in it, before calling.
   *
   * @param string|array|object $service
   *   A service class as a string, or an array with the service info, where:
   *   - the first element is a path to the service YAML file,
   *   - the second element - the service name.
   * @param string $eventName
   *   The Event name.
   * @param object $event
   *   The Event object.
   */
  public static function callEventSubscriber($service, string $eventName, object &$event): void {
    if (!is_object($service)) {
      if (is_array($service)) {
        [$servicesYamlFile, $serviceName] = $service;
        if (empty($serviceName)) {
          throw new \Exception("No service name is present in the service '$service'.");
        }
      }
      elseif (is_string($service)) {
        // Assuming that the service name is related to a called module.
        // Using there jumping to one level upper when detecting module info,
        // because current call adds a new step already.
        $servicesYamlFile = self::getModuleRoot(1) . DIRECTORY_SEPARATOR . self::getModuleName(1) . '.services.yml';
        $serviceName = $service;
      }
      else {
        throw new \Exception('The service parameter is in wrong format.');
      }
      $serviceInfo = self::getServiceInfoFromYaml($serviceName, $servicesYamlFile);

      // Checking the presence of the 'event_subscriber' tag.
      $tagFound = FALSE;
      foreach ($serviceInfo['tags'] ?? [] as $tag) {
        if ($tag['name'] == 'event_subscriber') {
          $tagFound = TRUE;
          break;
        }
      }
      if (!$tagFound) {
        throw new \Exception("EventSubscriber $serviceName misses the 'event_subscriber' tag in the service definition");
      }

      $service = self::initServiceFromYaml($servicesYamlFile, $serviceName);
    }
    $subscribedEvents = $service->getSubscribedEvents();
    self::callClassMethods($service, $subscribedEvents[$eventName], [$event]);
  }

  /* ************************************************************************ *
   * Wrappers for UnitTestCase functions to make them available statically.
   * ************************************************************************ */

  /**
   * Gets the random generator for the utility methods.
   *
   * @see \Drupal\Tests\UnitTestCase::getRandomGenerator()
   */
  public static function getRandomGenerator() {
    return UnitTestCaseWrapper::getInstance()->getRandomGenerator();
  }

  /**
   * Sets up a container with a cache tags invalidator.
   *
   * @see \Drupal\Tests\UnitTestCase::getContainerWithCacheTagsInvalidator()
   */
  public static function getContainerWithCacheTagsInvalidator(CacheTagsInvalidatorInterface $cache_tags_validator) {
    return UnitTestCaseWrapper::getInstance()->getContainerWithCacheTagsInvalidator($cache_tags_validator);
  }

  /**
   * Returns a stub class resolver.
   *
   * @see \Drupal\Tests\UnitTestCase::getClassResolverStub()
   */
  public static function getClassResolverStub() {
    return UnitTestCaseWrapper::getInstance()->getClassResolverStub();
  }

  /**
   * Returns a stub translation manager that just returns the passed string.
   *
   * @see \Drupal\Tests\UnitTestCase::getStringTranslationStub()
   */
  public static function getStringTranslationStub() {
    return UnitTestCaseWrapper::getInstance()->getStringTranslationStub();
  }

  /**
   * Returns a mock object for the specified class.
   *
   * @see \Drupal\Tests\UnitTestCase::createMock()
   */
  public static function createMock(string $originalClassName): MockObject {
    return UnitTestCaseWrapper::getInstance()->createMockWrapped($originalClassName);
  }

  /**
   * Returns a partial mock object for the specified class.
   *
   * @see \Drupal\Tests\UnitTestCase::createPartialMock()
   */
  public static function createPartialMock(string $originalClassName, array $methods): MockObject {
    return UnitTestCaseWrapper::getInstance()->createPartialMockWrapped($originalClassName, $methods);
  }

  /* ************************************************************************ *
   * UnitTestCase additions.
   * ************************************************************************ */

  /**
   * Creates a partial mock for the class and call constructor with arguments.
   */
  public static function createPartialMockWithConstructor(string $originalClassName, array $methods, ?array $constructorArgs = NULL, ?array $addMethods = NULL): MockObject {
    return UnitTestCaseWrapper::getInstance()->createPartialMockWithConstructor($originalClassName, $methods, $constructorArgs, $addMethods);
  }

  /**
   * Creates a partial mock with ability to add custom methods.
   */
  public static function createPartialMockWithCustomMethods(string $originalClassName, array $methods, ?array $addMethods = NULL): MockObject {
    return UnitTestCaseWrapper::getInstance()->createPartialMockWithCustomMethods($originalClassName, $methods, $addMethods);
  }

  /**
   * Sets an array as the iterator on a mocked object.
   *
   * @param array $array
   *   The array with data.
   * @param \PHPUnit\Framework\MockObject\MockObject $mock
   *   The mocked object.
   *
   * @return \PHPUnit\Framework\MockObject\MockObject
   *   The mocked object.
   */
  public static function addIteratorToMock(array $array, MockObject $mock): MockObject {
    $iterator = new \ArrayIterator($array);

    $mock->method('rewind')
      ->willReturnCallback(function () use ($iterator): void {
        $iterator->rewind();
      });

    $mock->method('current')
      ->willReturnCallback(function () use ($iterator) {
        return $iterator->current();
      });

    $mock->method('key')
      ->willReturnCallback(function () use ($iterator) {
        return $iterator->key();
      });

    $mock->method('next')
      ->willReturnCallback(function () use ($iterator): void {
        $iterator->next();
      });

    $mock->method('valid')
      ->willReturnCallback(function () use ($iterator): bool {
        return $iterator->valid();
      });

    $mock->method('offsetGet')
      ->willReturnCallback(function ($key) use ($iterator) {
        return $iterator[$key];
      });

    $mock->method('offsetSet')
      ->willReturnCallback(function ($key, $value) use ($iterator) {
        return $iterator[$key] = $value;
      });

    // @todo Check if the method getIterator is defined and mock it too.
    return $mock;
  }

  /**
   * Gets a module name from a namespace of a module class.
   *
   * @param string|int|null $namespaceOrLevel
   *   The module class namespace. If NULL - gets the namespace from a called
   *   function. If numeric - jumps upper the passed number of levels.
   *
   * @return string|null
   *   The module name, or NULL if can't find.
   */
  public static function getModuleName($namespaceOrLevel = NULL): ?string {
    $moduleName = NULL;
    if ($namespaceOrLevel === NULL || is_numeric($namespaceOrLevel)) {
      $level = is_numeric($namespaceOrLevel) ? 2 + $namespaceOrLevel : 2;
      $namespace = self::getCallerInfo($level)['class'];
    }
    else {
      $namespace = ltrim($namespaceOrLevel, '\\');
    }
    $parts = explode('\\', $namespace);
    if ($parts[0] === 'Drupal') {
      if ($parts[1] === 'Tests') {
        $moduleName = $parts[2];
      }
      else {
        $moduleName = $parts[1];
      }
    }

    if (
      in_array($moduleName, [
        'Component',
        'Core',
      ])) {
      $moduleName = 'core';
    }

    return $moduleName;
  }

  /**
   * Gets a root module folder from a module file full path.
   *
   * @param string|int|null $pathOrClassOrLevel
   *   A full path to a file or a class. If empty - gets the path of the
   *   function caller file.
   * @param string|null $moduleName
   *   The name of the module, if empty - gets the caller module name.
   *
   * @return string|null
   *   The full path to the module root.
   */
  public static function getModuleRoot($pathOrClassOrLevel = NULL, ?string $moduleName = NULL): ?string {
    if ($pathOrClassOrLevel === NULL || is_numeric($pathOrClassOrLevel)) {
      // Getting a module info from a caller function.
      $level = is_numeric($pathOrClassOrLevel) ? 2 + $pathOrClassOrLevel : 2;
      $callerInfo = self::getCallerInfo($level);
      $file = $callerInfo['file'];
      $moduleName = self::getModuleName($callerInfo['class']);
    }
    elseif (
      str_starts_with($pathOrClassOrLevel, 'Drupal\\')
      || str_starts_with($pathOrClassOrLevel, '\\Drupal\\')
    ) {
      // We have a full path of the Drupal class.
      $file = self::getClassFile($pathOrClassOrLevel);
      if (!$moduleName) {
        $moduleName = self::getModuleName($pathOrClassOrLevel);
      }
    }
    else {
      $file = $pathOrClassOrLevel;
    }
    $parts = explode(DIRECTORY_SEPARATOR, $file);

    // Trying to scan all upper directories and find module info file.
    $moduleInfoFile = $moduleName . '.info.yml';
    $coreRootInfoFile = 'core.services.yml';
    $index = count($parts);
    while ($index > 0) {
      $directory = implode(DIRECTORY_SEPARATOR, array_slice($parts, 0, $index));
      if (
        file_exists($directory . DIRECTORY_SEPARATOR . $moduleInfoFile)
        || file_exists($directory . DIRECTORY_SEPARATOR . $coreRootInfoFile)
      ) {
        return $directory;
      }
      $index--;
    }
    return NULL;
  }

  /**
   * Gets the absolute path to a file in the called module by a relative path.
   *
   * Usually used for working with module's YAML files, like
   * `config/install/my_module.settings.yml` or `my_module.links.menu.yml`.
   *
   * The module root is detected by the location of the file, from which this
   * function is called. Use $parentCallsLevel more than zero, if you call this
   * function from an intermediate class.
   *
   * @param string $relativePath
   *   A relative path to a file, from the module root directory.
   * @param int|null $parentCallsLevel
   *   An optional level to skip some parent calls, if you need to detect the
   *   module from a parent function, not from which you call this function.
   *
   * @return string
   *   A full path to the module file.
   */
  public static function getModuleFilePath(string $relativePath, ?int $parentCallsLevel = NULL) {
    // We should increase a level by one, to bypass this function call.
    $parentCallsLevel ??= 0;
    $parentCallsLevel++;
    $modulePath = TestHelpers::getModuleRoot($parentCallsLevel);
    return $modulePath . DIRECTORY_SEPARATOR . $relativePath;
  }

  /**
   * Gets the static storage for a mocked PHP function.
   *
   * @param string $functionPath
   *   A full path to a function, like \Drupal\my_module\MyFeature\fopen.
   *   Or a special value '__ALL__' to get all defined storages.
   *
   * @internal
   *   This function is a helper function for the mockPhpFunction().
   *
   * @return \Drupal\test_helpers\lib\MockedFunctionStorage|array
   *   A class with the function storage, or an array with storages if the
   *   $functionPath == '__ALL__'.
   */
  public static function mockPhpFunctionStorage(string $functionPath) {
    static $storages;
    if ($functionPath == '__ALL__') {
      return $storages;
    }
    if (!isset($storages[$functionPath])) {
      $storage = new MockedFunctionStorage();
      $storages[$functionPath] = $storage;
    }
    $storageReference = $storages[$functionPath];
    return $storageReference;
  }

  /**
   * Sets a mock for a PHP build-in function for the namespace of a class.
   *
   * Warning! The function will be mocked for all classes in the passed class
   * namespace, and will stay for all other test function in the file. So,
   * always use TestHelpers::unmockPhpFunction() at the end of each test.
   *
   * For your tests you call it in tearDownAfterClass() to always revert all
   * mocks after finishing the current unit test, to not affect next tests.
   *
   * @param string $name
   *   The function name.
   * @param string $class
   *   The full class name (FQCN) to get the namespace.
   * @param callable|null $callback
   *   A callback function to call, or NULL if no callback is needed.
   *
   * @return \Drupal\test_helpers\lib\MockedFunctionCalls
   *   A MockedFunctionCalls object, containing list of all function calls.
   */
  public static function mockPhpFunction(string $name, string $class, ?callable $callback = NULL): MockedFunctionCalls {
    $namespace = implode("\\", array_slice(explode("\\", ltrim($class, '\\')), 0, -1));
    $functionPath = $namespace . '\\' . $name;
    $storage = self::mockPhpFunctionStorage($functionPath);
    $storage->isUnmocked = FALSE;
    $storage->callback = $callback;
    $storage->calls = new MockedFunctionCalls();

    // If the mocked function is not defined yet, evaluating the dynamic
    // definition of it.
    if (!function_exists($functionPath)) {
      $code = <<<EOT
namespace $namespace;

use Drupal\\test_helpers\TestHelpers;

function $name() {
  \$storage = TestHelpers::mockPhpFunctionStorage('$functionPath');
  \$args = func_get_args();
  if (\$storage->isUnmocked == TRUE) {
    return \\$name(...\$args);
  }
  \$storage->calls[] = \$args;
  if (isset(\$storage->callback)) {
    \$callback = \$storage->callback;
    return \$callback(...\$args);
  }
}
EOT;
      // To suppress `The use of function eval() is discouraged` warning.
      // @codingStandardsIgnoreStart
      eval ($code);
      // @codingStandardsIgnoreEnd
    }
    return $storage->calls;
  }

  /**
   * Unmocks the previously mocked PHP build-in function in a namespace.
   *
   * @param string $name
   *   The function name.
   * @param string $class
   *   The full class name (FQCN) to get the namespace.
   */
  public static function unmockPhpFunction(string $name, string $class) {
    $namespace = implode("\\", array_slice(explode("\\", ltrim($class, '\\')), 0, -1));
    $functionPath = $namespace . '\\' . $name;
    $storage = self::mockPhpFunctionStorage($functionPath);
    $storage->isUnmocked = TRUE;
    $storage->calls = new MockedFunctionCalls();
  }

  /**
   * Unmocks all functions, that was mocked by mockPhpFunction().
   */
  public static function unmockAllPhpFunctions() {
    $storage = self::mockPhpFunctionStorage('__ALL__');
    foreach ($storage as $item) {
      $item->isUnmocked = TRUE;
      $item->calls = new MockedFunctionCalls();
    }
  }

  /* ************************************************************************ *
   * Internal functions.
   * ************************************************************************ */

  /**
   * An internal callback helper function for array_uintersect.
   *
   * @internal For internal usage only.
   */
  private static function isValueSubsetOfCallback($expected, $value): int {
    // The callback function for array_uintersect should return
    // integer instead of bool (-1, 0, 1).
    if (is_array($expected)) {
      return self::isNestedArraySubsetOf($value, $expected) ? 0 : -1;
    }
    return ($value == $expected) ? 0 : -1;
  }

  /**
   * Replaces all objects to string representation in a nested array.
   *
   * @param array $array
   *   The array to use.
   *
   * @return array
   *   A copy of the array with replaced objects to strings.
   *
   * @internal For internal usage only.
   */
  private static function arrayObjectsToStrings(array $array): array {
    $arrayCopy = $array;
    // $arrayCopy = json_decode(json_encode($array), TRUE);
    array_walk_recursive($arrayCopy, self::class . '::arrayObjectsToStringsCallback');
    return $arrayCopy;
  }

  /**
   * Makes an in-place replacement of an object to string in an array item.
   *
   * An internal callback helper function for arrayObjectsToStrings.
   *
   * @param mixed $item
   *   An array item value.
   *
   * @internal For internal usage only.
   */
  private static function arrayObjectsToStringsCallback(&$item) {
    if (is_object($item)) {
      $item = '[object] ' . get_class($item) . ', id ' . spl_object_id($item);
    }
  }

  /**
   * An improved version of var_export that outputs arrays in short format.
   *
   * @param mixed $value
   *   A value to use.
   * @param mixed $return
   *   If used and set to true, will return the variable representation instead
   *   of outputting it.
   *
   * @return string|null
   *   A string representation of the value, if $return is true.
   *
   * @internal For internal usage only.
   */
  private static function shorthandVarExport($value, $return = FALSE) {
    $export = var_export($value, TRUE);
    $patterns = [
      "/array \(/" => '[',
      "/^([ ]*)\)(,?)$/m" => '$1]$2',
      "/\s\=\>\s+\n\s+\[/" => ' => [',
      "/\[\n\s+\]/" => '[]',
    ];
    $output = preg_replace(array_keys($patterns), array_values($patterns), $export);
    if ($return) {
      return $output;
    }
    else {
      echo $output;
    }
  }

  /**
   * Load params from YAML file to the current service container.
   *
   * @param string $file
   *   A path to a YAML file.
   * @param bool $override
   *   A flag to override the current parameters.
   */
  public static function loadParametersFromYamlFile(string $file, bool $override = TRUE): void {
    $container = self::getContainer();
    $content = self::parseYamlFile($file);
    foreach ($content['parameters'] ?? [] as $key => $value) {
      if ($override || !$container->hasParameter($key)) {
        $container->setParameter($key, $value);
      }
    }
  }

  /**
   * Load params from a JSON string.
   *
   * @param string $json
   *   A JSON string.
   * @param bool $override
   *   A flag to override the current parameters.
   *
   * @internal For internal usage only.
   */
  private static function loadParametersFromJson(string $json, bool $override = TRUE): void {
    $container = self::getContainer();
    $content = json_decode($json, JSON_OBJECT_AS_ARRAY);
    if ($content === NULL) {
      throw new \Exception("The parameters JSON data in the file CoreFeaturesMap.[CORE_VERSION].php is invalid: $json");
    }
    foreach ($content ?? [] as $key => $value) {
      if ($override || !$container->hasParameter($key)) {
        $container->setParameter($key, $value);
      }
    }
  }

  /**
   * Gets a service info from a YAML file.
   *
   * @internal For internal usage only.
   */
  private static function getServiceInfoFromYaml(string $serviceName, string $servicesYamlFile, bool $skipLoadingParams = FALSE): array {
    $filePath = (str_starts_with($servicesYamlFile, DIRECTORY_SEPARATOR) ? '' : self::getDrupalRoot()) . DIRECTORY_SEPARATOR . $servicesYamlFile;
    $info = self::getServiceInfo($serviceName, $filePath);
    if (!$skipLoadingParams) {
      self::loadParametersFromYamlFile($filePath);
    }
    return $info;
  }

  /**
   * Converts a condition in Search API format to the associative array.
   *
   * @internal For internal usage only.
   */
  private static function conditionsSearchApiObjectsToArray(array $conditionsAsObjects): array {
    $conditions = [];
    foreach ($conditionsAsObjects as $delta => $conditionAsObject) {
      $conditions[$delta] = [
        'field' => $conditionAsObject->getField(),
        'value' => $conditionAsObject->getValue(),
        'operator' => $conditionAsObject->getOperator(),
      ];
    }
    return $conditions;
  }

  /**
   * Gets a service class by name, using Drupal defaults or a custom YAML file.
   *
   * @internal For internal usage only.
   */
  private static function getServiceInfo(string $serviceName, ?string $servicesYamlFile = NULL): array {
    if ($serviceName == 'kernel') {
      $info = [
        'class' => DrupalKernel::class,
      ];
      return $info;
    }
    if ($servicesYamlFile === NULL) {
      self::requireCoreFeaturesMap();
      if (isset(TEST_HELPERS_DRUPAL_CORE_SERVICE_MAP[$serviceName])) {
        $file = self::getDrupalRoot() . DIRECTORY_SEPARATOR . TEST_HELPERS_DRUPAL_CORE_SERVICE_MAP[$serviceName];
      }
      // If we have a path to a Drupal class.
      else {
        // Trying to auto detect the location of the services file.
        $calledModulePath = self::getModuleRoot();
        $calledModuleName = self::getModuleName();

        // For cases when we have an intermediate call from the test_helpers
        // itself.
        if ($calledModuleName == 'test_helpers') {
          $depth = 2;
          do {
            $calledModulePath2 = self::getModuleRoot($depth);
            $calledModuleName2 = self::getModuleName($depth);
            $depth++;
          } while (
            $calledModuleName2 == 'test_helpers'
            && $depth < 10
          );
          $fileParentModule = "$calledModulePath2/$calledModuleName2.services.yml";
        }
        $file = "$calledModulePath/$calledModuleName.services.yml";
      }
    }
    else {
      $file = (str_starts_with($servicesYamlFile, DIRECTORY_SEPARATOR) ? '' : self::getDrupalRoot()) . DIRECTORY_SEPARATOR . $servicesYamlFile;
    }
    $serviceYaml = self::parseYamlFile($file);
    if (
      !isset($serviceYaml['services'][$serviceName])
      && isset($fileParentModule)
    ) {
      $serviceYaml = self::parseYamlFile($fileParentModule);
    }
    if (isset($serviceYaml['services'][$serviceName])) {
      $info = $serviceYaml['services'][$serviceName];
    }
    elseif (isset(self::SERVICES_CUSTOM_STUBS[$serviceName])) {
      $info = [
        'class' => self::SERVICES_CUSTOM_STUBS[$serviceName],
      ];
    }
    else {
      if ($servicesYamlFile !== NULL && !file_exists($file)) {
        throw new \Exception("The services file '$file' is not found.");
      }
      else {
        throw new \Exception("The service '$serviceName' is not found in the list of core services and in the current module file ($file).");
      }
    }
    if (isset($info['parent'])) {
      $infoParent = self::getServiceInfo($info['parent']);
      if (!isset($info['class'])) {
        $info['class'] = $infoParent['class'];
      }
      if (isset($infoParent['arguments'])) {
        $info['arguments'] =
          [...$infoParent['arguments'], ...($info['arguments'] ?? [])];
      }
      if (isset($infoParent['factory'])) {
        $info['factory'] = $infoParent['factory'];
      }
    }
    if (!isset($info['class'])) {
      $info['class'] = $serviceName;
    }
    return $info;
  }

  /**
   * Loads a Drupal Core services map file for the correct Drupal Core version.
   *
   * @internal
   *   This function is used mostly for the internal functionality.
   */
  public static function requireCoreFeaturesMap(): void {
    if (defined('TEST_HELPERS_DRUPAL_CORE_SERVICE_MAP')) {
      return;
    }
    $mapDirectory = dirname(__FILE__) . '/lib/CoreFeaturesMaps';
    $filePrefix = 'CoreFeaturesMap';
    $parts = explode('.', \Drupal::VERSION);
    if (isset($parts[2])) {
      // We have a Semantic Version number.
      [$major, $minor, $patch] = $parts;
      unset($patch);
    }
    else {
      // We have a Dev version.
      $major = $parts[0];
      [$minor] = explode('-', $parts[1]);
    }
    while ($major >= 8) {
      $path = "$mapDirectory/$filePrefix.$major.$minor.php";
      if (file_exists($path)) {
        break;
      }
      $minor--;
      if ($minor < 0) {
        $major--;
        $minor = 10;
      }
    }
    if (!isset($path)) {
      throw new \Exception("The Core Features Map file is not found.");
    }
    require_once $path;
  }

  /**
   * Calls class methods from the passed list.
   *
   * A helper function for testing event subscribers.
   *
   * @param object $class
   *   The class to use.
   * @param mixed $methods
   *   The list of methods to call. Can be a string or array, supported formats:
   *   - 'methodName'
   *   - ['methodName', $priority]
   *   - [['methodName1', $priority], ['methodName2']].
   * @param array $arguments
   *   Arguments to pass to the method.
   *
   * @internal For internal usage only.
   */
  private static function callClassMethods(object $class, $methods, array $arguments = []) {
    $methodsToCall = [];
    if (is_string($methods)) {
      // When a single method is passed as string.
      $methodsToCall[] = $methods;
    }
    elseif (is_numeric($methods[1] ?? NULL)) {
      // When a single method is passed as array with function and priority.
      $methodsToCall[$methods[1]] = $methods[0];
    }
    else {
      // When a list of methods is passed as array.
      foreach ($methods as $method) {
        if (is_string($method)) {
          $methodsToCall[] = $method;
        }
        elseif (is_array($method)) {
          if (isset($method[1])) {
            $methodsToCall[$method[1]] = $method[0];
          }
          else {
            $methodsToCall[] = $method[0];
          }
        }
      }
    }
    ksort($methodsToCall);
    foreach ($methodsToCall as $method) {
      $class->$method(...$arguments);
    }
  }

  /**
   * Gets a filename of a caller (parent) function.
   *
   * @param int $level
   *   The level to use when getting a filename. By default '2' to get parent of
   *   parent caller, because for parent caller it's easier to use __FILE__
   *   construction.
   *
   * @return array
   *   An array with the caller information:
   *   - file: the full path to file.
   *   - function: the function name.
   *   - class: the full class name.
   *
   * @internal
   *   This function is used mostly for the internal functionality.
   */
  public static function getCallerInfo(int $level = 2): ?array {
    $backtrace = debug_backtrace(defined("DEBUG_BACKTRACE_IGNORE_ARGS") ? DEBUG_BACKTRACE_IGNORE_ARGS : FALSE);
    // The caller filename is located in one level lower.
    $callerTrace = $backtrace[$level - 1] ?? NULL;
    // The caller filename is located in one level lower.
    $calledTrace = $backtrace[$level] ?? NULL;
    if (!$callerTrace || !$calledTrace) {
      return NULL;
    }
    return [
      'file' => $callerTrace['file'] ?? NULL,
      'function' => $calledTrace['function'],
      'class' => $calledTrace['class'],
    ];
  }

  /**
   * Parses a YAML file and caches the result in memory.
   *
   * @param string $servicesFile
   *   A path to a YAML file.
   *
   * @return mixed
   *   A result of file parsing.
   *
   * @internal For internal usage only.
   */
  private static function parseYamlFile(string $servicesFile) {
    static $cache;
    $cache ??= [];
    if (isset($cache[$servicesFile])) {
      return $cache[$servicesFile];
    }
    if (file_exists($servicesFile)) {
      $cache[$servicesFile] = Yaml::parseFile($servicesFile, Yaml::PARSE_CUSTOM_TAGS);
    }
    return $cache[$servicesFile] ?? NULL;
  }

  /**
   * Disables a constructor calls to allow only static calls.
   *
   * @codeCoverageIgnore
   *
   * @internal For internal usage only.
   */
  private function __construct() {
  }

  /**
   * Throws a user error with explanation of a failing match.
   *
   * @param string $subject
   *   The name of the property that was checked.
   * @param mixed $expected
   *   The expected value.
   * @param mixed $actual
   *   The actual value.
   *
   * @internal For internal usage only.
   */
  private static function throwMatchError(string $subject, $expected, $actual) {
    trigger_error(
      "The $subject doesn't match, expected: "
      . self::shorthandVarExport(is_array($expected) ? self::arrayObjectsToStrings($expected) : $expected, TRUE)
      . "\nactual: " . self::shorthandVarExport(is_array($actual) ? self::arrayObjectsToStrings($actual) : $actual, TRUE), E_USER_NOTICE);
  }

  /**
   * Throws a user error with a message.
   *
   * @param string $message
   *   A message to throw.
   *
   * @internal For internal usage to throw user errors.
   */
  public static function throwUserError(string $message): void {
    trigger_error($message, E_USER_NOTICE);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc