accountSwitcher = $accountSwitcher; $this->entityTypeBundleInfo = $bundleInfo; $this->entityTypeManager = $entityTypeManager; $this->logger = $logger; $this->root = $root; $this->serializer = $serializer; $this->eventDispatcher = $eventDispatcher; $this->setPath($path); } /** * Gets a hash of the content entity definitions on the site. * * @param string $requestedTypeName * If specified, the types hash will only contain the key for that type. * * @return array * A machine-name-indexed hash of entity type definitions. If $typeName was * not specified, the definitions are returned for all entity types. */ public function contentEntityTypes(string $requestedTypeName = NULL) { $definitions = $this->entityTypeManager->getDefinitions(); $entityTypes = []; foreach ($definitions as $machine => $type) { $class = $type->getClass(); $implements = class_implements($class); if (isset($implements[static::CONTENT_INTERFACE])) { $entityTypes[$machine] = $type; } } if (!empty($requestedTypeName)) { $entityTypes = [$requestedTypeName => $entityTypes[$requestedTypeName]]; } return $entityTypes; } /** * Dump the entities in the selected bundle of the entity type. * * @param string $typeName * The name of the entity type from which to dump a bundle. * @param \Drupal\Core\Entity\EntityTypeInterface $entityType * The entity type instance. * @param string $bundle * The name of the bundle to dump. Assumed to be valid. * * @see \Drupal\reinstall\Dumper::validateBundles() */ protected function dumpEntitiesBundle($typeName, EntityTypeInterface $entityType, string $bundle) { $t0 = microtime(TRUE); $storage = $this->entityTypeManager->getStorage($typeName); $bundleKey = $this->entityTypeManager->getDefinition($typeName)->getKey('bundle'); $countQuery = $storage->getQuery(); if ($bundleKey) { $countQuery = $countQuery->condition($bundleKey, $bundle); } $count = $countQuery ->count() ->execute(); $query = $storage->getQuery(); if ($bundleKey) { $query = $query->condition($bundleKey, $bundle); } $ids = $query->execute(); $chunks = array_chunk($ids, static::BATCH_SIZE); $path = $this->prepareDestination($typeName, $bundle); $fp = fopen($path, "w"); foreach ($chunks as $chunk) { $this->dumpEntitiesChunk($fp, $typeName, $bundle, $storage, $chunk); } // Ensure files always contain at least an empty array. if (empty($chunks)) { fwrite($fp, '{ }'); } fclose($fp); $t1 = microtime(TRUE); $this->logger->info('Dumped @count entities for @type/@bundle in @sec seconds.', [ '@count' => $count, '@type' => $typeName, '@bundle' => $bundle, '@sec' => sprintf('%.1f', $t1 - $t0), ]); } /** * Dump a chunk of entities from a common bundle in an existing opened file. * * @param resource $fp * A file pointer to which to write. * @param string $typeName * The name of the entity type from which to dump a chunk of entities. * @param string $bundle * The bundle name. * @param \Drupal\Core\Entity\EntityStorageInterface $storage * The entity storage for the entity type. * @param array $chunk * An array of entities to dump. */ protected function dumpEntitiesChunk( $fp, string $typeName, string $bundle, EntityStorageInterface $storage, array $chunk ) { $entities = $storage->loadMultiple($chunk); // Allow adding data to entities before exporting, like term parents. $eventPre = new DumperEvent($storage, $bundle, $entities); $this->eventDispatcher->dispatch(ReinstallEvents::PRE_DUMP, $eventPre); $this->dumpEntities($fp, $typeName, $bundle, $entities); // Allow extra work after exporting, like copying files. $eventPost = new DumperEvent($storage, $bundle, $entities); $this->eventDispatcher->dispatch(ReinstallEvents::POST_DUMP, $eventPost); } /** * Load entities for a given entity bundle. * * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type * The entity type object. * @param \Drupal\Core\Entity\ContentEntityStorageInterface $storage * The entity storage for the entity type. * * @return array * A hash of entities by id. */ protected function loadMultiBundleEntities(ContentEntityTypeInterface $type, ContentEntityStorageInterface $storage ) { $bundleNames = array_keys($this->entityTypeBundleInfo->getBundleInfo($type->id())); $key = $type->getKey('bundle'); $entities = []; foreach ($bundleNames as $bundleName) { $bundleEntities = $storage->loadByProperties([$key => $bundleName]); $entities[$bundleName] = $bundleEntities; } return $entities; } /** * Dump entities to YAML files. * * @param string $typeName * The name of the entity type for which to export entities. * @param array $bundles * Optional. An array of bundles to export. Only used if an entity type is * specified, since $requestedTypeName is itself optional. * * @see https://www.drupal.org/node/218104 */ public function dump($typeName, array $bundles = []) { $this->accountSwitcher->switchTo(new UserSession(['uid' => 1])); $entityType = current($this->contentEntityTypes($typeName)); $validBundles = $this->validateBundles($typeName, $bundles); foreach ($validBundles as $bundle) { $this->dumpEntitiesBundle($typeName, $entityType, $bundle); } $this->accountSwitcher->switchBack(); } /** * Generate import YAML for entities. * * @param resource $fp * The file into which to write. * @param string $entityTypeName * The entity type. * @param string $bundleName * The bundle name. * @param array $entities * The entities. */ public function dumpEntities($fp, string $entityTypeName, string $bundleName, array $entities) { $array = $this->toArray($entities); fwrite($fp, Yaml::dump($array, static::INLINE_DEPTH, 2)); fflush($fp); } /** * Prepare the dump destination directory and return the file name within it. * * @param string $entityTypeName * The type of the entities to dump. * @param string $bundleName * The bundle of the entities to dump. * * @return string * The path of the dump file. */ protected function prepareDestination(string $entityTypeName, string $bundleName): string { $importPath = $this->importPath; $dir = "$importPath/$entityTypeName"; if (!file_exists($dir)) { mkdir($dir, 0777, TRUE); } $path = "${dir}/${bundleName}.yml"; return $path; } /** * Store the absolute path to the import directory. * * @param string $path * * The Drupal-root-relative import path. * * @throws \InvalidArgumentException * If the directory does not exist. */ public function setPath(string $path) { $completePath = $this->root . '/' . $path; $real = realpath($completePath); if (!is_dir($real)) { drupal_set_message("Non-existent base dump directory: $completePath.", "error"); throw new \InvalidArgumentException("Non-existent base dump directory: $completePath."); } $this->importPath = $real; } /** * Like NormalizerInterface::normalize(), but for an array. * * @param array $entities * The entities to convert to arrays. * * @return mixed * The array representing the entities. */ protected function toArray(array $entities): array { $json_options = []; $json = $this->serializer->serialize($entities, 'json', $json_options); $hash = json_decode($json, TRUE); return $hash; } /** * Deduplicate the bundles list and remove invalid bundle names. * * @param string $entityTypeName * The name of the entity type for which to validate bundle names. * @param array $bundles * Bundle names to be validated. * * @return array * Valid bundles to dump. */ protected function validateBundles(string $entityTypeName, array $bundles) { sort($bundles); $bundleInfo = $this->entityTypeBundleInfo->getBundleInfo($entityTypeName); // Note: array_flip will ensure requested bundles are only asked once. $flippedBundles = array_flip($bundles); $validFlippedBundles = array_intersect_key($flippedBundles, $bundleInfo); $uniqueValidBundles = array_flip($validFlippedBundles); sort($uniqueValidBundles); if ($bundles !== $uniqueValidBundles) { throw new \InvalidArgumentException( "\nRequested bundles: " . implode(', ', $bundles) . "\nValid bundles: " . implode(', ', $uniqueValidBundles) . "\n"); } if (empty($uniqueValidBundles)) { $uniqueValidBundles = array_keys($bundleInfo); } return $uniqueValidBundles; } }