Dumper.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. <?php
  2. namespace Drupal\reinstall;
  3. use Drupal\Component\Utility\Unicode;
  4. use Drupal\Core\Entity\ContentEntityStorageInterface;
  5. use Drupal\Core\Entity\ContentEntityTypeInterface;
  6. use Drupal\Core\Entity\EntityTypeBundleInfo;
  7. use Drupal\Core\Entity\EntityTypeManagerInterface;
  8. use Drupal\Core\Session\AccountSwitcherInterface;
  9. use Drupal\Core\Session\UserSession;
  10. use Drupal\file\Entity\File;
  11. use Symfony\Component\Serializer\Serializer;
  12. use Symfony\Component\Yaml\Yaml;
  13. /**
  14. * Class Dumper provides export for content entities.
  15. */
  16. class Dumper {
  17. const CONTENT_INTERFACE = 'Drupal\Core\Entity\ContentEntityInterface';
  18. /**
  19. * The path to the data files to import, relative to app.root.
  20. */
  21. const IMPORT_PATH = '../data';
  22. /**
  23. * The structure depth at which the YAML dump switched to inline format.
  24. */
  25. const INLINE_DEPTH = 5;
  26. /**
  27. * The account_switcher service.
  28. *
  29. * @var \Drupal\Core\Session\AccountSwitcherInterface
  30. */
  31. protected $accountSwitcher;
  32. /**
  33. * The entity_type.bundle_info service.
  34. *
  35. * @var \Drupal\Core\Entity\EntityTypeBundleInfo
  36. */
  37. protected $entityTypeBundleInfo;
  38. /**
  39. * The entity_type.manager service.
  40. *
  41. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  42. */
  43. protected $entityTypeManager;
  44. /**
  45. * The actual path from which to perform imports.
  46. *
  47. * Derived from @app_root and %rdcm.reinstall_path%.
  48. *
  49. * @var string
  50. */
  51. protected $importPath = self::IMPORT_PATH;
  52. /**
  53. * The app.root parameter service.
  54. *
  55. * @var string
  56. */
  57. protected $root;
  58. /**
  59. * The serializer service.
  60. *
  61. * @var \Symfony\Component\Serializer\Serializer
  62. */
  63. protected $serializer;
  64. /**
  65. * Dumper constructor.
  66. *
  67. * @param \Drupal\Core\Session\AccountSwitcherInterface $accountSwitcher
  68. * The account_switcher service.
  69. * @param \Drupal\Core\Entity\EntityTypeBundleInfo $bundleInfo
  70. * The entity_type.bundle_info service.
  71. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
  72. * The entity_type.manager service.
  73. * @param string $root
  74. * The value of the app.root "parameter service".
  75. * @param \Symfony\Component\Serializer\Serializer $serializer
  76. * The serializer service.
  77. */
  78. public function __construct(
  79. AccountSwitcherInterface $accountSwitcher,
  80. EntityTypeBundleInfo $bundleInfo,
  81. EntityTypeManagerInterface $entityTypeManager,
  82. string $root,
  83. Serializer $serializer,
  84. string $importPath
  85. ) {
  86. $this->accountSwitcher = $accountSwitcher;
  87. $this->entityTypeBundleInfo = $bundleInfo;
  88. $this->entityTypeManager = $entityTypeManager;
  89. $this->root = $root;
  90. $this->serializer = $serializer;
  91. $this->setPath($importPath);
  92. }
  93. /**
  94. * Gets a hash of the content entity definitions on the site.
  95. *
  96. * @param string $requestedTypeName
  97. * If specified, the types hash will only contain the key for that type.
  98. *
  99. * @return array
  100. * A machine-name-indexed hash of entity type definitions. If $typeName was
  101. * not specified, the definitions are returned for all entity types.
  102. */
  103. public function contentEntityTypes(string $requestedTypeName = NULL) {
  104. $definitions = $this->entityTypeManager->getDefinitions();
  105. $entityTypes = [];
  106. foreach ($definitions as $machine => $type) {
  107. $class = $type->getClass();
  108. $implements = class_implements($class);
  109. if (isset($implements[static::CONTENT_INTERFACE])) {
  110. $entityTypes[$machine] = $type;
  111. }
  112. }
  113. if (!empty($requestedTypeName)) {
  114. $entityTypes = [$requestedTypeName => $entityTypes[$requestedTypeName]];
  115. }
  116. return $entityTypes;
  117. }
  118. /**
  119. * Load entities for a given entity bundle.
  120. *
  121. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
  122. * The entity type object.
  123. * @param \Drupal\Core\Entity\ContentEntityStorageInterface $storage
  124. * The entity storage for the entity type.
  125. *
  126. * @return array
  127. * A hash of entities by id.
  128. */
  129. protected function loadMultiBundleEntities(ContentEntityTypeInterface $type,
  130. ContentEntityStorageInterface $storage
  131. ) {
  132. $bundleNames = array_keys($this->entityTypeBundleInfo->getBundleInfo($type->id()));
  133. $key = $type->getKey('bundle');
  134. $entities = [];
  135. foreach ($bundleNames as $bundleName) {
  136. $bundleEntities = $storage->loadByProperties([$key => $bundleName]);
  137. $entities[$bundleName] = $bundleEntities;
  138. }
  139. return $entities;
  140. }
  141. /**
  142. * Dump entities to YAML files.
  143. *
  144. * @param string $requestedTypeName
  145. * Optional. The name of the entity type for which to export entities. If
  146. * absent, all entities in all types are dumped.
  147. *
  148. * @see https://www.drupal.org/node/218104
  149. */
  150. public function dump($requestedTypeName = NULL) {
  151. $this->accountSwitcher->switchTo(new UserSession(['uid' => 1]));
  152. $entityTypes = $this->contentEntityTypes($requestedTypeName);
  153. foreach ($entityTypes as $entityTypeName => $entityType) {
  154. $storage = $this->entityTypeManager->getStorage($entityTypeName);
  155. $entities = $entityType->hasKey('bundle')
  156. ? $this->loadMultiBundleEntities($entityType, $storage)
  157. : [$entityTypeName => $storage->loadMultiple()];
  158. foreach ($entities as $bundleName => $bundleEntities) {
  159. $this->dumpEntities($bundleEntities, $entityTypeName, $bundleName);
  160. if ($entityTypeName === 'file') {
  161. $this->dumpFiles($bundleEntities, $bundleName);
  162. }
  163. }
  164. }
  165. $this->accountSwitcher->switchBack();
  166. }
  167. /**
  168. * Generate import YAML for entities.
  169. *
  170. * @param array $entities
  171. * The entities.
  172. * @param string $entity_type
  173. * The entity type.
  174. * @param string $bundle
  175. * The bundle name.
  176. */
  177. public function dumpEntities(array $entities, string $entity_type, string $bundle = NULL) {
  178. if (!$bundle) {
  179. $bundle = $entity_type;
  180. }
  181. $json_options = [];
  182. $json = $this->serializer->serialize($entities, 'json', $json_options);
  183. $hash = json_decode($json, TRUE);
  184. $importPath = $this->importPath;
  185. $dir = "$importPath/$entity_type";
  186. if (!file_exists($dir)) {
  187. mkdir($dir, 0777, TRUE);
  188. }
  189. $path = "${dir}/${bundle}.yml";
  190. file_put_contents($path, Yaml::dump($hash, static::INLINE_DEPTH, 2));
  191. }
  192. public function dumpFiles(array $files, string $bundle) {
  193. $importPath = $this->importPath;
  194. $dir = "$importPath/file";
  195. $usedNamespaces = array_keys(array_reduce($files, [$this, 'namespacesReducer'], []));
  196. $lists = [];
  197. foreach ($usedNamespaces as $ns) {
  198. // XXX Consider using \0 to support xargs: file names MAY contain spaces.
  199. $path = "$dir/$ns.list.txt";
  200. $nsDir = "$dir/$ns";
  201. if (!is_dir($nsDir)) {
  202. echo "Creating $nsDir\n";
  203. mkdir($nsDir, 0777, TRUE);
  204. }
  205. // fopen() is in text mode by default.
  206. $lists[$ns] = [
  207. 'dir' => $nsDir,
  208. 'handle' => fopen($path, 'w'),
  209. ];
  210. }
  211. /**
  212. * @var int $fid
  213. * @var \Drupal\file\Entity\File $file
  214. */
  215. foreach ($files as $fid => $file) {
  216. $uri = $file->getFileUri();
  217. $target = file_uri_target($uri);
  218. fputs($lists[$ns]['handle'], $target . "\n");
  219. $dest = $lists[$ns]['dir'] . '/' . $target;
  220. $dir = dirname($dest);
  221. if (!is_dir($dir)) {
  222. mkdir($dir, 0777, TRUE);
  223. }
  224. file_unmanaged_copy($uri, $dest, FILE_EXISTS_REPLACE);
  225. }
  226. foreach ($lists as $list) {
  227. fclose($list['handle']);
  228. }
  229. }
  230. /**
  231. * array_reduce() callback to collect namespaces from file entities.
  232. *
  233. * @param string[] $accu
  234. * @param \Drupal\file\Entity\File $fileItem
  235. *
  236. * @return string[]
  237. *
  238. * @see \Drupal\reinstall\Dumper::dumpFiles()
  239. */
  240. protected function namespacesReducer(array $accu, File $fileItem) {
  241. $uri = $fileItem->getFileUri();
  242. // Plain filenames without a namespace. Should not happen, but...
  243. if (FALSE === ($len = Unicode::strpos($uri, '://'))) {
  244. return $accu;
  245. };
  246. $namespace = Unicode::substr($uri, 0, $len);
  247. $accu[$namespace] = TRUE;
  248. return $accu;
  249. }
  250. public function setPath(string $path) {
  251. $completePath = $this->root . '/' . $path;
  252. $real = realpath($completePath);
  253. if (!is_dir($real)) {
  254. drupal_set_message("Non-existent base dump directory: $completePath.", "error");
  255. throw new \InvalidArgumentException("Non-existent base dump directory: $completePath.");
  256. return;
  257. }
  258. $this->importPath = $real;
  259. }
  260. }