|
@@ -0,0 +1,301 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Drupal\reinstall;
|
|
|
+
|
|
|
+use Drupal\Component\Utility\Unicode;
|
|
|
+use Drupal\Core\Entity\ContentEntityStorageInterface;
|
|
|
+use Drupal\Core\Entity\ContentEntityTypeInterface;
|
|
|
+use Drupal\Core\Entity\EntityTypeBundleInfo;
|
|
|
+use Drupal\Core\Entity\EntityTypeManagerInterface;
|
|
|
+use Drupal\Core\Session\AccountSwitcherInterface;
|
|
|
+use Drupal\Core\Session\UserSession;
|
|
|
+use Drupal\file\Entity\File;
|
|
|
+use Symfony\Component\Serializer\Serializer;
|
|
|
+use Symfony\Component\Yaml\Yaml;
|
|
|
+
|
|
|
+
|
|
|
+ * Class Dumper provides export for content entities.
|
|
|
+ */
|
|
|
+class Dumper {
|
|
|
+
|
|
|
+ const CONTENT_INTERFACE = 'Drupal\Core\Entity\ContentEntityInterface';
|
|
|
+
|
|
|
+
|
|
|
+ * The path to the data files to import, relative to app.root.
|
|
|
+ */
|
|
|
+ const IMPORT_PATH = '../data';
|
|
|
+
|
|
|
+
|
|
|
+ * The structure depth at which the YAML dump switched to inline format.
|
|
|
+ */
|
|
|
+ const INLINE_DEPTH = 5;
|
|
|
+
|
|
|
+
|
|
|
+ * The account_switcher service.
|
|
|
+ *
|
|
|
+ * @var \Drupal\Core\Session\AccountSwitcherInterface
|
|
|
+ */
|
|
|
+ protected $accountSwitcher;
|
|
|
+
|
|
|
+
|
|
|
+ * The entity_type.bundle_info service.
|
|
|
+ *
|
|
|
+ * @var \Drupal\Core\Entity\EntityTypeBundleInfo
|
|
|
+ */
|
|
|
+
|
|
|
+ protected $entityTypeBundleInfo;
|
|
|
+
|
|
|
+
|
|
|
+ * The entity_type.manager service.
|
|
|
+ *
|
|
|
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
|
|
+ */
|
|
|
+ protected $entityTypeManager;
|
|
|
+
|
|
|
+
|
|
|
+ * The actual path from which to perform imports.
|
|
|
+ *
|
|
|
+ * Derived from @app_root and %rdcm.reinstall_path%.
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $importPath = self::IMPORT_PATH;
|
|
|
+
|
|
|
+
|
|
|
+ * The app.root parameter service.
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $root;
|
|
|
+
|
|
|
+
|
|
|
+ * The serializer service.
|
|
|
+ *
|
|
|
+ * @var \Symfony\Component\Serializer\Serializer
|
|
|
+ */
|
|
|
+ protected $serializer;
|
|
|
+
|
|
|
+
|
|
|
+ * Dumper constructor.
|
|
|
+ *
|
|
|
+ * @param \Drupal\Core\Session\AccountSwitcherInterface $accountSwitcher
|
|
|
+ * The account_switcher service.
|
|
|
+ * @param \Drupal\Core\Entity\EntityTypeBundleInfo $bundleInfo
|
|
|
+ * The entity_type.bundle_info service.
|
|
|
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
|
|
|
+ * The entity_type.manager service.
|
|
|
+ * @param string $root
|
|
|
+ * The value of the app.root "parameter service".
|
|
|
+ * @param \Symfony\Component\Serializer\Serializer $serializer
|
|
|
+ * The serializer service.
|
|
|
+ */
|
|
|
+ public function __construct(
|
|
|
+ AccountSwitcherInterface $accountSwitcher,
|
|
|
+ EntityTypeBundleInfo $bundleInfo,
|
|
|
+ EntityTypeManagerInterface $entityTypeManager,
|
|
|
+ string $root,
|
|
|
+ Serializer $serializer,
|
|
|
+ string $importPath
|
|
|
+ ) {
|
|
|
+ $this->accountSwitcher = $accountSwitcher;
|
|
|
+ $this->entityTypeBundleInfo = $bundleInfo;
|
|
|
+ $this->entityTypeManager = $entityTypeManager;
|
|
|
+ $this->root = $root;
|
|
|
+ $this->serializer = $serializer;
|
|
|
+
|
|
|
+ $this->setPath($importPath);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 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;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 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 $requestedTypeName
|
|
|
+ * Optional. The name of the entity type for which to export entities. If
|
|
|
+ * absent, all entities in all types are dumped.
|
|
|
+ *
|
|
|
+ * @see https:
|
|
|
+ */
|
|
|
+ public function dump($requestedTypeName = NULL) {
|
|
|
+ $this->accountSwitcher->switchTo(new UserSession(['uid' => 1]));
|
|
|
+
|
|
|
+ $entityTypes = $this->contentEntityTypes($requestedTypeName);
|
|
|
+ foreach ($entityTypes as $entityTypeName => $entityType) {
|
|
|
+ $storage = $this->entityTypeManager->getStorage($entityTypeName);
|
|
|
+ $entities = $entityType->hasKey('bundle')
|
|
|
+ ? $this->loadMultiBundleEntities($entityType, $storage)
|
|
|
+ : [$entityTypeName => $storage->loadMultiple()];
|
|
|
+
|
|
|
+ foreach ($entities as $bundleName => $bundleEntities) {
|
|
|
+ $this->dumpEntities($bundleEntities, $entityTypeName, $bundleName);
|
|
|
+ if ($entityTypeName === 'file') {
|
|
|
+ $this->dumpFiles($bundleEntities, $bundleName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $this->accountSwitcher->switchBack();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * Generate import YAML for entities.
|
|
|
+ *
|
|
|
+ * @param array $entities
|
|
|
+ * The entities.
|
|
|
+ * @param string $entity_type
|
|
|
+ * The entity type.
|
|
|
+ * @param string $bundle
|
|
|
+ * The bundle name.
|
|
|
+ */
|
|
|
+ public function dumpEntities(array $entities, string $entity_type, string $bundle = NULL) {
|
|
|
+ if (!$bundle) {
|
|
|
+ $bundle = $entity_type;
|
|
|
+ }
|
|
|
+
|
|
|
+ $json_options = [];
|
|
|
+ $json = $this->serializer->serialize($entities, 'json', $json_options);
|
|
|
+ $hash = json_decode($json, TRUE);
|
|
|
+
|
|
|
+ $importPath = $this->importPath;
|
|
|
+ $dir = "$importPath/$entity_type";
|
|
|
+ if (!file_exists($dir)) {
|
|
|
+ mkdir($dir, 0777, TRUE);
|
|
|
+ }
|
|
|
+
|
|
|
+ $path = "${dir}/${bundle}.yml";
|
|
|
+
|
|
|
+ file_put_contents($path, Yaml::dump($hash, static::INLINE_DEPTH, 2));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function dumpFiles(array $files, string $bundle) {
|
|
|
+ $importPath = $this->importPath;
|
|
|
+ $dir = "$importPath/file";
|
|
|
+ $usedNamespaces = array_keys(array_reduce($files, [$this, 'namespacesReducer'], []));
|
|
|
+ $lists = [];
|
|
|
+
|
|
|
+ foreach ($usedNamespaces as $ns) {
|
|
|
+
|
|
|
+ $path = "$dir/$ns.list.txt";
|
|
|
+
|
|
|
+ $nsDir = "$dir/$ns";
|
|
|
+ if (!is_dir($nsDir)) {
|
|
|
+ echo "Creating $nsDir\n";
|
|
|
+ mkdir($nsDir, 0777, TRUE);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ $lists[$ns] = [
|
|
|
+ 'dir' => $nsDir,
|
|
|
+ 'handle' => fopen($path, 'w'),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * @var int $fid
|
|
|
+ * @var \Drupal\file\Entity\File $file
|
|
|
+ */
|
|
|
+ foreach ($files as $fid => $file) {
|
|
|
+ $uri = $file->getFileUri();
|
|
|
+ $target = file_uri_target($uri);
|
|
|
+ fputs($lists[$ns]['handle'], $target . "\n");
|
|
|
+ $dest = $lists[$ns]['dir'] . '/' . $target;
|
|
|
+
|
|
|
+ $dir = dirname($dest);
|
|
|
+ if (!is_dir($dir)) {
|
|
|
+ mkdir($dir, 0777, TRUE);
|
|
|
+ }
|
|
|
+ file_unmanaged_copy($uri, $dest, FILE_EXISTS_REPLACE);
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($lists as $list) {
|
|
|
+ fclose($list['handle']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ * array_reduce() callback to collect namespaces from file entities.
|
|
|
+ *
|
|
|
+ * @param string[] $accu
|
|
|
+ * @param \Drupal\file\Entity\File $fileItem
|
|
|
+ *
|
|
|
+ * @return string[]
|
|
|
+ *
|
|
|
+ * @see \Drupal\reinstall\Dumper::dumpFiles()
|
|
|
+ */
|
|
|
+ protected function namespacesReducer(array $accu, File $fileItem) {
|
|
|
+ $uri = $fileItem->getFileUri();
|
|
|
+
|
|
|
+ if (FALSE === ($len = Unicode::strpos($uri, '://'))) {
|
|
|
+ return $accu;
|
|
|
+ };
|
|
|
+
|
|
|
+ $namespace = Unicode::substr($uri, 0, $len);
|
|
|
+ $accu[$namespace] = TRUE;
|
|
|
+ return $accu;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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.");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->importPath = $real;
|
|
|
+ }
|
|
|
+}
|