test_helpers-1.0.0-alpha6/src/StubFactory/EntityStorageStubFactory.php

src/StubFactory/EntityStorageStubFactory.php
<?php

namespace Drupal\test_helpers\StubFactory;

use Drupal\Core\Config\Entity\Query\QueryFactory;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\test_helpers\TestHelpers;

/**
 * A stub of the Drupal's default SqlContentEntityStorage class.
 *
 * @package TestHelpers\DrupalServiceStubFactories
 */
class EntityStorageStubFactory {
  /**
   * A static storage for entity data per entity type.
   *
   * @var array
   */
  private static array $entityDataStorage = [];

  /**
   * Disables the constructor to use only static methods.
   */
  private function __construct() {
  }

  /**
   * Creates a new Entity Storage Stub object.
   *
   * @param string $entityClassOrName
   *   The original class to use for stub, or an entity type for types in.
   * @param string $annotation
   *   The annotation to use. If missing - tries ContentEntityType and
   *   ConfigEntityType.
   *   Examples:
   *   - ContentEntityType
   *   - ConfigEntityType
   *   or other annotations.
   * @param array|null $storageOptions
   *   The array of options:
   *   - constructorArguments: additional arguments to the constructor.
   *   - 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.
   *   - skipModuleFile: skips including of ".module" file.
   *
   * @throws \Exception
   *   When the annotation cannot be parsed.
   *
   * @return \Drupal\Core\Entity\EntityStorageInterface|\PHPUnit\Framework\MockObject\MockObject
   *   The mocked Entity Storage Stub.
   */
  public static function create(string $entityClassOrName, ?string $annotation = NULL, ?array $storageOptions = NULL) {
    $storageOptions ??= [];
    if (is_array($storageOptions['methods'] ?? NULL)) {
      @trigger_error('The storage option "methods" is deprecated in test_helpers:1.0.0-beta9 and is removed from test_helpers:1.0.0-rc1. Use "mockMethods" instead. See https://www.drupal.org/project/test_helpers/issues/3347857', E_USER_DEPRECATED);
      $storageOptions['mockMethods'] = array_unique(array_merge($storageOptions['mockMethods'] ?? [], $storageOptions['methods']));
    }
    TestHelpers::requireCoreFeaturesMap();
    $entityClass = ltrim(TEST_HELPERS_DRUPAL_CORE_STORAGE_MAP[$entityClassOrName] ?? $entityClassOrName, '\\');

    if ($annotation) {
      $entityTypeDefinition = TestHelpers::getPluginDefinition($entityClass, 'Entity', $annotation);
    }
    else {
      // Starting with the Content Entity type at first.
      $annotation = 'ContentEntityType';
      $entityTypeDefinition = TestHelpers::getPluginDefinition($entityClass, 'Entity', $annotation);
      if ($entityTypeDefinition == NULL) {
        // If it fails - try Config Entity type.
        $annotation = 'ConfigEntityType';
        $entityTypeDefinition = TestHelpers::getPluginDefinition($entityClass, 'Entity', $annotation);
      }
      if ($entityTypeDefinition == NULL) {
        throw new \Error("Can't detect the entity type definition for the class $entityClass");
      }
    }

    if ($entityTypeDefinition == NULL) {
      throw new \Exception("Can't parse annotation for class $entityClass using the annotation $annotation");
    }

    $entityTypeId = $entityTypeDefinition->id();

    // Some entity types depends on hook functions in the module file,
    // so trying to include this file.
    // @todo Add an option to disable this.
    if (!($storageOptions['skipModuleFile'] ?? NULL) && $entityFile = TestHelpers::getClassFile($entityClass)) {
      $moduleDirectory = dirname(dirname(dirname($entityFile)));
      $moduleName = basename($moduleDirectory);
      $moduleFile = "$moduleDirectory/$moduleName.module";
      file_exists($moduleFile) && include_once $moduleFile;
    }

    $entityTypeStorageClass = $entityTypeDefinition->getStorageClass();
    self::$entityDataStorage ??= [
      'value' => [],
      'maxId' => [],
      'maxRevisionId' => [],
    ];
    $entitiesStorage = &self::$entityDataStorage['value'][$entityTypeId];
    $entitiesStorage = [];
    $entitiesMaxIdStorage = &self::$entityDataStorage['maxId'][$entityTypeId];
    $entitiesMaxIdStorage = 0;
    $entitiesMaxRevisionIdStorage = &self::$entityDataStorage['maxRevisionId'][$entityTypeId];
    $entitiesMaxRevisionIdStorage = 0;
    TestHelpers::service('entity_field.manager')->stubClearFieldDefinitions($entityTypeId);
    TestHelpers::service('entity_type.manager')->stubSetDefinition($entityTypeId, $entityTypeDefinition);

    $constructArguments = NULL;

    if ($storageOptions['constructorArguments'] ?? NULL) {
      $constructArguments = $storageOptions['constructorArguments'];
    }
    $overriddenMethods = [];

    // We override the save function, but depends on the entity type and
    // options, the function name can be different.
    $saveFunctionName = 'save';

    switch ($annotation) {
      case 'ContentEntityType':
        $constructArguments ??= [
          $entityTypeDefinition,
          TestHelpers::service('database'),
          TestHelpers::service('entity_field.manager'),
          TestHelpers::service('cache.entity'),
          TestHelpers::service('language_manager'),
          TestHelpers::service('entity.memory_cache'),
          TestHelpers::service('entity_type.bundle.info'),
          TestHelpers::service('entity_type.manager'),
        ];
        $overriddenMethods[] = 'loadMultiple';
        // In some cases the method loadRevision() doesn't exist.
        if (method_exists($entityTypeStorageClass, 'loadRevision')) {
          $overriddenMethods[] = 'loadRevision';
        }
        $overriddenMethods[] = 'delete';

        if (!($storageOptions['skipPrePostSave'] ?? NULL)) {
          // The ContentEntityStorageBase has a method doSaveFieldItems()
          // but it can be absent in some entity types.
          // If not, fall back to the function doSave().
          if (method_exists($entityTypeStorageClass, 'doSaveFieldItems')) {
            $saveFunctionName = 'doSaveFieldItems';
          }
          else {
            $saveFunctionName = 'doSave';
          }
        }
        $overriddenMethods[] = $saveFunctionName;
        break;

      case 'ConfigEntityType':
        switch ($entityClass) {
          case "Drupal\Core\Field\Entity\BaseFieldOverride":
            break;

          default:
            if ($storageOptions['skipPrePostSave'] ?? NULL) {
              $overriddenMethods[] = 'loadMultiple';
              $overriddenMethods[] = 'delete';
              $overriddenMethods[] = $saveFunctionName;
            }
            TestHelpers::service('module_handler');

            TestHelpers::service(
              'entity.query.config',
              new QueryFactory(
                TestHelpers::service('config.factory'),
                TestHelpers::service('keyvalue'),
                TestHelpers::service('config.manager')
              )
            );

            $constructArguments ??= [
              $entityTypeDefinition,
              TestHelpers::service('config.factory'),
              TestHelpers::service('uuid'),
              TestHelpers::service('language_manager'),
              TestHelpers::service('entity.memory_cache'),
            ];
            break;
        }
        break;

      default:
        throw new \Error('Unsupported entity type annotation: ' . $annotation);
    }

    $addMethods = array_unique(
      [
        ...($storageOptions['addMethods'] ?? []),
        'stubGetAllLatestRevision',
      ]
    );

    $mockMethods = array_unique(array_merge($overriddenMethods, $storageOptions['mockMethods'] ?? []));

    // Removing requested mocked methods from mocking by the current class.
    $overriddenMethods = array_diff(
      $overriddenMethods,
      [...$storageOptions['mockMethods'] ?? [], ...$addMethods]
    );
    /** @var \Drupal\Core\Entity\EntityStorageInterface|\PHPUnit\Framework\MockObject\MockObject $entityStorage */
    if ($constructArguments) {
      $entityStorage = TestHelpers::createPartialMockWithConstructor(
        $entityTypeStorageClass,
        $mockMethods,
        $constructArguments,
        $addMethods,
      );
      $entityStorage->setModuleHandler(TestHelpers::service('module_handler'));
    }
    else {
      // Custom constructor.
      $entityStorage = TestHelpers::createPartialMockWithCustomMethods(
        $entityTypeStorageClass,
        $mockMethods,
        [
          ...$addMethods,
          'stubInit',
        ],
      );

      TestHelpers::setMockedClassMethod(
        $entityStorage,
        'stubInit',
        function () use ($entityTypeDefinition, $annotation) {
          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          $this->entityType = $entityTypeDefinition;
          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          $this->entityTypeId = $this->entityType->id();

          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          if (property_exists($this, 'baseEntityClass')) {
            // @phpstan-ignore-next-line `$this` will be available in the runtime.
            $this->baseEntityClass = $this->entityType->getClass();
          }
          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          if (property_exists($this, 'entityTypeBundleInfo')) {
            // @phpstan-ignore-next-line `$this` will be available in the runtime.
            $this->entityTypeBundleInfo = TestHelpers::service('entity_type.bundle.info');
          }

          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          if (property_exists($this, 'database')) {
            // @phpstan-ignore-next-line `$this` will be available in the runtime.
            $this->database = TestHelpers::service('database');
          }
          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          if (property_exists($this, 'memoryCache')) {
            // @phpstan-ignore-next-line `$this` will be available in the runtime.
            $this->memoryCache = TestHelpers::service('cache.backend.memory')->get('entity_storage_stub.memory_cache.' . $this->entityTypeId);
          }
          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          if (property_exists($this, 'cacheBackend')) {
            // @phpstan-ignore-next-line `$this` will be available in the runtime.
            $this->cacheBackend = TestHelpers::service('cache.backend.memory')->get('entity_storage_stub.cache.' . $this->entityTypeId);
          }

          // @todo Rework this to a more correct way.
          if ($annotation == 'ConfigEntityType') {
            // @phpstan-ignore-next-line `$this` will be available in the runtime.
            if (property_exists($this, 'uuidService')) {
              // @phpstan-ignore-next-line `$this` will be available in the runtime.
              $this->uuidService = TestHelpers::service('uuid');
              // @phpstan-ignore-next-line `$this` will be available in the runtime.
            }
          }
        },
      );
      $entityStorage->stubInit();
    }

    if (in_array($saveFunctionName, $overriddenMethods)) {
      $saveFunction = function (EntityInterface $entity, array $names = []) use (&$entitiesStorage, &$entitiesMaxIdStorage, &$entitiesMaxRevisionIdStorage) {
        /**
         * @var \Drupal\test_helpers\Stub\EntityStubInterface $this
         */
        // @phpstan-ignore-next-line `$this` will be available in the runtime.
        $idProperty = $this->entityType->getKey('id') ?? NULL;
        if ($idProperty) {
          // The `id` value for even integer autoincrement is stored as string
          // in Drupal, so we should follow this behavior too.
          // @todo Make detection of the id field type, and calculate only for
          // integers.
          $id = (string) EntityStorageStubFactory::processAutoincrementId($entitiesMaxIdStorage, $entity->id());
          if (isset($entity->$idProperty)) {
            $entity->$idProperty = $id;
          }
          else {
            // For ConfigEntityType the uuid is protected.
            TestHelpers::setPrivateProperty($entity, $idProperty, $id);
          }
        }

        // @phpstan-ignore-next-line `$this` will be available in the runtime.
        $uuidProperty = $this->entityType->getKey('uuid') ?? NULL;
        if ($uuidProperty && empty($entity->uuid())) {
          $uuid = TestHelpers::service('uuid')->generate();
          if (isset($entity->$uuidProperty)) {
            $entity->$uuidProperty = $uuid;
          }
          else {
            // For ConfigEntityType the uuid is protected.
            TestHelpers::setPrivateProperty($entity, $uuidProperty, $uuid);
          }
        }

        // @phpstan-ignore-next-line `$this` will be available in the runtime.
        if (($this->entityType instanceof ContentEntityTypeInterface) && $this->entityType->isRevisionable()) {
          $setRevisionId = function ($entity, $revisionId) {
            // @phpstan-ignore-next-line `$this` will be available in the runtime.
            $revisionProperty = $this->entityType->getKey('revision') ?? NULL;
            $entityKeys = TestHelpers::getPrivateProperty($entity, 'entityKeys');
            $entityKeys['revision'] = $revisionId;
            TestHelpers::setPrivateProperty($entity, 'entityKeys', $entityKeys);
            if (isset($entity->$revisionProperty)) {
              $entity->$revisionProperty = $revisionId;
            }
            else {
              // For ConfigEntityType the uuid is protected.
              TestHelpers::setPrivateProperty($entity, $revisionProperty, $revisionId);
            }
          };

          if ($entity->isNewRevision()) {
            $revisionId = EntityStorageStubFactory::processAutoincrementId($entitiesMaxRevisionIdStorage);
            $setRevisionId($entity, $revisionId);
          }
          else {
            $revisionId = $entity->getRevisionId();
          }
          if ($entity instanceof TranslatableInterface) {
            foreach ($entity->getTranslationLanguages() as $langcode => $language) {
              if ($entityInLanguage = $entity->getTranslation($langcode)) {
                $setRevisionId($entityInLanguage, $revisionId);
              }
            }
          }
        }

        // For content entities we should look all translations.
        if ($entity instanceof TranslatableInterface) {
          $entityData = [];
          foreach ($entity->getTranslationLanguages() as $langcode => $language) {
            if (!$entityInLanguage = $entity->getTranslation($langcode)) {
              break;
            }
            $entityData['#translations'][$langcode] = EntityStorageStubFactory::entityToValues($entityInLanguage);
          }
        }
        else {
          $entityData = EntityStorageStubFactory::entityToValues($entity);
        }

        // @phpstan-ignore-next-line `$this` will be available in the runtime.
        if ($this->entityType instanceof ContentEntityTypeInterface) {
          $entitiesStorage['byRevisionId'][$entity->getRevisionId()] = $entityData;
          if ($entity->isLatestRevision()) {
            $entitiesStorage['byIdLatestRevision'][$entity->id()] = $entityData;
            if (
              $entity->isNew()
              || $entity->isDefaultRevision()
              || !isset($entitiesStorage['byId'][$entity->id()])) {
              $entitiesStorage['byId'][$entity->id()] = $entityData;
            }
          }
        }
        else {
          $entitiesStorage['byId'][$entity->id()] = $entityData;

        }
      };
      TestHelpers::setMockedClassMethod($entityStorage, $saveFunctionName, $saveFunction);
    }

    if (in_array('delete', $overriddenMethods)) {
      TestHelpers::setMockedClassMethod(
        $entityStorage, 'delete', function (array $entities) use (&$entitiesStorage) {
          foreach ($entities as $entity) {
            $id = $entity->id();
            if (isset($entitiesStorage['byId'][$id])) {
              unset($entitiesStorage['byId'][$id]);
            }
          }
        }
      );
    }

    if (in_array('loadMultiple', $overriddenMethods)) {
      TestHelpers::setMockedClassMethod(
        $entityStorage, 'loadMultiple', function (?array $ids = NULL) use (&$entitiesStorage) {
          if ($ids === NULL) {
            $entitiesValues = $entitiesStorage['byId'] ?? [];
          }
          else {
            $entitiesValues = [];
            foreach ($ids as $id) {
              if (isset($entitiesStorage['byId'][$id])) {
                $entitiesValues[] = $entitiesStorage['byId'][$id];
              }
            }
          }

          $entities = [];
          foreach ($entitiesValues as $values) {
            // @phpstan-ignore-next-line `$this` will be available in the runtime.
            $entity = EntityStorageStubFactory::valuesToEntity($this->entityType, $values);
            // @phpstan-ignore-next-line `$this` will be available in the runtime.
            if (($this->entityType instanceof ContentEntityTypeInterface) && $this->entityType->isRevisionable()) {
              $entity->updateLoadedRevisionId();
            }
            $entities[$entity->id()] = $entity;
          }

          return $entities;
        }
      );
    }

    if (in_array('loadRevision', $overriddenMethods)) {
      TestHelpers::setMockedClassMethod(
        $entityStorage, 'loadRevision', function ($id) use (&$entitiesStorage) {
          if (!$values = $entitiesStorage['byRevisionId'][$id] ?? NULL) {
            return NULL;
          }
          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          $entity = EntityStorageStubFactory::valuesToEntity($this->entityType, $values);
          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          if (($this->entityType instanceof ContentEntityTypeInterface) && $this->entityType->isRevisionable()) {
            $entity->updateLoadedRevisionId();
          }
          return $entity;
        }
      );
    }
    TestHelpers::setMockedClassMethod(
      $entityStorage, 'stubGetAllLatestRevision', function () use (&$entitiesStorage) {
        $entities = [];
        foreach ($entitiesStorage['byIdLatestRevision'] ?? [] as $values) {
          // @phpstan-ignore-next-line `$this` will be available in the runtime.
          $entities[] = EntityStorageStubFactory::valuesToEntity($this->entityType, $values);
        }
        return $entities;
      }
    );

    // Crunches for known specific Core entity types.
    switch ($entityTypeId) {
      case 'block_content':
        // This service is required for preSave() to work well.
        TestHelpers::service('plugin.manager.block');
        break;

    }

    return $entityStorage;
  }

  /**
   * Converts entity to an array with values.
   *
   * @param mixed $entity
   *   The entity to use.
   *
   * @return array
   *   The array with values.
   */
  public static function entityToValues($entity) {
    $values = $entity->toArray();
    $keys = $entity->getEntityType()->getKeys();
    foreach ($keys as $key) {
      if (empty($values[$key])) {
        unset($values[$key]);
      }
    }
    return $values;
  }

  /**
   * Creates an entity from values array.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entityType
   *   The entity type to use.
   * @param array $values
   *   The values to use.
   *
   * @return \Drupal\Core\Entity\EntityTypeInterface
   *   The created entity
   */
  public static function valuesToEntity(EntityTypeInterface $entityType, array $values = []): EntityInterface {
    if ($values['#translations'] ?? NULL) {
      $defaultLanguageCode = TestHelpers::service('language.default')->get()->getId();
      $defaultTranslationValues =
        $values['#translations'][$defaultLanguageCode]
        ?? $values['#translations'][LanguageInterface::LANGCODE_NOT_SPECIFIED]
        ?? [];

      // This trick is required when the default language is changed.
      if (isset($defaultTranslationValues['default_langcode'])) {
        unset($defaultTranslationValues['default_langcode']);
      }

      $entity = TestHelpers::createEntity($entityType->getClass(), $defaultTranslationValues);
      $entity->enforceIsNew(FALSE);
      foreach ($values['#translations'] ?? [] as $langCode => $valuesInLang) {
        if (
          $langCode != $defaultLanguageCode
          && $langCode != LanguageInterface::LANGCODE_NOT_SPECIFIED
        ) {
          $entity->addTranslation($langCode, $valuesInLang);
        }
      }
    }
    else {
      $entity = TestHelpers::createEntity($entityType->getClass(), $values);
    }
    if (($entityType instanceof ContentEntityTypeInterface) && $entityType->isRevisionable()) {
      $entity->updateLoadedRevisionId();
    }
    return $entity;
  }

  /**
   * Processes the autoincrement id to generate next values correctly.
   *
   * @param mixed $storage
   *   A static storage to use.
   * @param int|string $currentId
   *   The current id to use, or NULL to get the next autoincrement value.
   *
   * @return int|string
   *   The passed value or autoincrement, if passed is NULL.
   */
  public static function processAutoincrementId(&$storage, $currentId = NULL) {
    if ($currentId) {
      if ($currentId > $storage) {
        $storage = $currentId;
      }
      $id = $currentId;
    }
    else {
      $id = ++$storage;
    }
    return $id;
  }

}

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

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