Dumper.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <?php
  2. namespace Drupal\reinstall;
  3. use Drupal\Core\Entity\ContentEntityStorageInterface;
  4. use Drupal\Core\Entity\ContentEntityTypeInterface;
  5. use Drupal\Core\Entity\EntityStorageInterface;
  6. use Drupal\Core\Entity\EntityTypeBundleInfo;
  7. use Drupal\Core\Entity\EntityTypeInterface;
  8. use Drupal\Core\Entity\EntityTypeManagerInterface;
  9. use Drupal\Core\Session\AccountSwitcherInterface;
  10. use Drupal\Core\Session\UserSession;
  11. use Psr\Log\LoggerInterface;
  12. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  13. use Symfony\Component\Serializer\Serializer;
  14. use Symfony\Component\Yaml\Yaml;
  15. /**
  16. * Class Dumper provides export for content entities.
  17. *
  18. * @see \Drupal\reinstall\ReinstallEvents
  19. */
  20. class Dumper {
  21. /**
  22. * Should probably be a service parameter in the future.
  23. */
  24. const BATCH_SIZE = 50;
  25. /**
  26. * The interface used to ensure the dump request is for content entities.
  27. */
  28. const CONTENT_INTERFACE = 'Drupal\Core\Entity\ContentEntityInterface';
  29. /**
  30. * The path to the data files to import, relative to app.root.
  31. */
  32. const IMPORT_PATH = '../data';
  33. /**
  34. * The structure depth at which the YAML dump switched to inline format.
  35. */
  36. const INLINE_DEPTH = 5;
  37. /**
  38. * The account_switcher service.
  39. *
  40. * @var \Drupal\Core\Session\AccountSwitcherInterface
  41. */
  42. protected $accountSwitcher;
  43. /**
  44. * The entity_type.bundle_info service.
  45. *
  46. * @var \Drupal\Core\Entity\EntityTypeBundleInfo
  47. */
  48. protected $entityTypeBundleInfo;
  49. /**
  50. * The entity_type.manager service.
  51. *
  52. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  53. */
  54. protected $entityTypeManager;
  55. /**
  56. * The event_dispatcher service.
  57. *
  58. * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
  59. */
  60. protected $eventDispatcher;
  61. /**
  62. * The actual path from which to perform imports.
  63. *
  64. * Derived from @app_root and %rdcm.reinstall_path%.
  65. *
  66. * @var string
  67. */
  68. protected $importPath = self::IMPORT_PATH;
  69. /**
  70. * The reinstall logger channel service.
  71. *
  72. * @var \Psr\Log\LoggerInterface
  73. */
  74. protected $logger;
  75. /**
  76. * The app.root parameter service.
  77. *
  78. * @var string
  79. */
  80. protected $root;
  81. /**
  82. * The serializer service.
  83. *
  84. * @var \Symfony\Component\Serializer\Serializer
  85. */
  86. protected $serializer;
  87. /**
  88. * Dumper constructor.
  89. *
  90. * @param \Drupal\Core\Session\AccountSwitcherInterface $accountSwitcher
  91. * The account_switcher service.
  92. * @param \Drupal\Core\Entity\EntityTypeBundleInfo $bundleInfo
  93. * The entity_type.bundle_info service.
  94. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
  95. * The entity_type.manager service.
  96. * @param string $root
  97. * The value of the app.root "parameter service".
  98. * @param \Symfony\Component\Serializer\Serializer $serializer
  99. * The serializer service.
  100. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
  101. * The event_dispatcher service.
  102. * @param string $path
  103. * The import path.
  104. * @param \Psr\Log\LoggerInterface $logger
  105. * The reinstall logger channel service.
  106. */
  107. public function __construct(
  108. AccountSwitcherInterface $accountSwitcher,
  109. EntityTypeBundleInfo $bundleInfo,
  110. EntityTypeManagerInterface $entityTypeManager,
  111. string $root,
  112. Serializer $serializer,
  113. EventDispatcherInterface $eventDispatcher,
  114. string $path,
  115. LoggerInterface $logger
  116. ) {
  117. $this->accountSwitcher = $accountSwitcher;
  118. $this->entityTypeBundleInfo = $bundleInfo;
  119. $this->entityTypeManager = $entityTypeManager;
  120. $this->logger = $logger;
  121. $this->root = $root;
  122. $this->serializer = $serializer;
  123. $this->eventDispatcher = $eventDispatcher;
  124. $this->setPath($path);
  125. }
  126. /**
  127. * Gets a hash of the content entity definitions on the site.
  128. *
  129. * @param string $requestedTypeName
  130. * If specified, the types hash will only contain the key for that type.
  131. *
  132. * @return array
  133. * A machine-name-indexed hash of entity type definitions. If $typeName was
  134. * not specified, the definitions are returned for all entity types.
  135. */
  136. public function contentEntityTypes(string $requestedTypeName = NULL) {
  137. $definitions = $this->entityTypeManager->getDefinitions();
  138. $entityTypes = [];
  139. foreach ($definitions as $machine => $type) {
  140. $class = $type->getClass();
  141. $implements = class_implements($class);
  142. if (isset($implements[static::CONTENT_INTERFACE])) {
  143. $entityTypes[$machine] = $type;
  144. }
  145. }
  146. if (!empty($requestedTypeName)) {
  147. $entityTypes = [$requestedTypeName => $entityTypes[$requestedTypeName]];
  148. }
  149. return $entityTypes;
  150. }
  151. /**
  152. * Dump the entities in the selected bundle of the entity type.
  153. *
  154. * @param string $typeName
  155. * The name of the entity type from which to dump a bundle.
  156. * @param \Drupal\Core\Entity\EntityTypeInterface $entityType
  157. * The entity type instance.
  158. * @param string $bundle
  159. * The name of the bundle to dump. Assumed to be valid.
  160. *
  161. * @see \Drupal\reinstall\Dumper::validateBundles()
  162. */
  163. protected function dumpEntitiesBundle($typeName, EntityTypeInterface $entityType, string $bundle) {
  164. $t0 = microtime(TRUE);
  165. $storage = $this->entityTypeManager->getStorage($typeName);
  166. $bundleKey = $this->entityTypeManager->getDefinition($typeName)->getKey('bundle');
  167. $countQuery = $storage->getQuery();
  168. if ($bundleKey) {
  169. $countQuery = $countQuery->condition($bundleKey, $bundle);
  170. }
  171. $count = $countQuery
  172. ->count()
  173. ->execute();
  174. $query = $storage->getQuery();
  175. if ($bundleKey) {
  176. $query = $query->condition($bundleKey, $bundle);
  177. }
  178. $ids = $query->execute();
  179. $chunks = array_chunk($ids, static::BATCH_SIZE);
  180. $path = $this->prepareDestination($typeName, $bundle);
  181. $fp = fopen($path, "w");
  182. foreach ($chunks as $chunk) {
  183. $this->dumpEntitiesChunk($fp, $typeName, $bundle, $storage, $chunk);
  184. }
  185. // Ensure files always contain at least an empty array.
  186. if (empty($chunks)) {
  187. fwrite($fp, '{ }');
  188. }
  189. fclose($fp);
  190. $t1 = microtime(TRUE);
  191. $this->logger->info('Dumped @count entities for @type/@bundle in @sec seconds.', [
  192. '@count' => $count,
  193. '@type' => $typeName,
  194. '@bundle' => $bundle,
  195. '@sec' => sprintf('%.1f', $t1 - $t0),
  196. ]);
  197. }
  198. /**
  199. * Dump a chunk of entities from a common bundle in an existing opened file.
  200. *
  201. * @param resource $fp
  202. * A file pointer to which to write.
  203. * @param string $typeName
  204. * The name of the entity type from which to dump a chunk of entities.
  205. * @param string $bundle
  206. * The bundle name.
  207. * @param \Drupal\Core\Entity\EntityStorageInterface $storage
  208. * The entity storage for the entity type.
  209. * @param array $chunk
  210. * An array of entities to dump.
  211. */
  212. protected function dumpEntitiesChunk(
  213. $fp,
  214. string $typeName,
  215. string $bundle,
  216. EntityStorageInterface $storage,
  217. array $chunk
  218. ) {
  219. $entities = $storage->loadMultiple($chunk);
  220. // Allow adding data to entities before exporting, like term parents.
  221. $eventPre = new DumperEvent($storage, $bundle, $entities);
  222. $this->eventDispatcher->dispatch(ReinstallEvents::PRE_DUMP, $eventPre);
  223. $this->dumpEntities($fp, $typeName, $bundle, $entities);
  224. // Allow extra work after exporting, like copying files.
  225. $eventPost = new DumperEvent($storage, $bundle, $entities);
  226. $this->eventDispatcher->dispatch(ReinstallEvents::POST_DUMP, $eventPost);
  227. }
  228. /**
  229. * Load entities for a given entity bundle.
  230. *
  231. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
  232. * The entity type object.
  233. * @param \Drupal\Core\Entity\ContentEntityStorageInterface $storage
  234. * The entity storage for the entity type.
  235. *
  236. * @return array
  237. * A hash of entities by id.
  238. */
  239. protected function loadMultiBundleEntities(ContentEntityTypeInterface $type,
  240. ContentEntityStorageInterface $storage
  241. ) {
  242. $bundleNames = array_keys($this->entityTypeBundleInfo->getBundleInfo($type->id()));
  243. $key = $type->getKey('bundle');
  244. $entities = [];
  245. foreach ($bundleNames as $bundleName) {
  246. $bundleEntities = $storage->loadByProperties([$key => $bundleName]);
  247. $entities[$bundleName] = $bundleEntities;
  248. }
  249. return $entities;
  250. }
  251. /**
  252. * Dump entities to YAML files.
  253. *
  254. * @param string $typeName
  255. * The name of the entity type for which to export entities.
  256. * @param array $bundles
  257. * Optional. An array of bundles to export. Only used if an entity type is
  258. * specified, since $requestedTypeName is itself optional.
  259. *
  260. * @see https://www.drupal.org/node/218104
  261. */
  262. public function dump($typeName, array $bundles = []) {
  263. $this->accountSwitcher->switchTo(new UserSession(['uid' => 1]));
  264. $entityType = current($this->contentEntityTypes($typeName));
  265. $validBundles = $this->validateBundles($typeName, $bundles);
  266. foreach ($validBundles as $bundle) {
  267. $this->dumpEntitiesBundle($typeName, $entityType, $bundle);
  268. }
  269. $this->accountSwitcher->switchBack();
  270. }
  271. /**
  272. * Generate import YAML for entities.
  273. *
  274. * @param resource $fp
  275. * The file into which to write.
  276. * @param string $entityTypeName
  277. * The entity type.
  278. * @param string $bundleName
  279. * The bundle name.
  280. * @param array $entities
  281. * The entities.
  282. */
  283. public function dumpEntities($fp, string $entityTypeName, string $bundleName, array $entities) {
  284. $array = $this->toArray($entities);
  285. fwrite($fp, Yaml::dump($array, static::INLINE_DEPTH, 2));
  286. fflush($fp);
  287. }
  288. /**
  289. * Prepare the dump destination directory and return the file name within it.
  290. *
  291. * @param string $entityTypeName
  292. * The type of the entities to dump.
  293. * @param string $bundleName
  294. * The bundle of the entities to dump.
  295. *
  296. * @return string
  297. * The path of the dump file.
  298. */
  299. protected function prepareDestination(string $entityTypeName, string $bundleName): string {
  300. $importPath = $this->importPath;
  301. $dir = "$importPath/$entityTypeName";
  302. if (!file_exists($dir)) {
  303. mkdir($dir, 0777, TRUE);
  304. }
  305. $path = "${dir}/${bundleName}.yml";
  306. return $path;
  307. }
  308. /**
  309. * Store the absolute path to the import directory.
  310. *
  311. * @param string $path
  312. *
  313. * The Drupal-root-relative import path.
  314. *
  315. * @throws \InvalidArgumentException
  316. * If the directory does not exist.
  317. */
  318. public function setPath(string $path) {
  319. $completePath = $this->root . '/' . $path;
  320. $real = realpath($completePath);
  321. if (!is_dir($real)) {
  322. drupal_set_message("Non-existent base dump directory: $completePath.", "error");
  323. throw new \InvalidArgumentException("Non-existent base dump directory: $completePath.");
  324. }
  325. $this->importPath = $real;
  326. }
  327. /**
  328. * Like NormalizerInterface::normalize(), but for an array.
  329. *
  330. * @param array $entities
  331. * The entities to convert to arrays.
  332. *
  333. * @return mixed
  334. * The array representing the entities.
  335. */
  336. protected function toArray(array $entities): array {
  337. $json_options = [];
  338. $json = $this->serializer->serialize($entities, 'json', $json_options);
  339. $hash = json_decode($json, TRUE);
  340. return $hash;
  341. }
  342. /**
  343. * Deduplicate the bundles list and remove invalid bundle names.
  344. *
  345. * @param string $entityTypeName
  346. * The name of the entity type for which to validate bundle names.
  347. * @param array $bundles
  348. * Bundle names to be validated.
  349. *
  350. * @return array
  351. * Valid bundles to dump.
  352. */
  353. protected function validateBundles(string $entityTypeName, array $bundles) {
  354. sort($bundles);
  355. $bundleInfo = $this->entityTypeBundleInfo->getBundleInfo($entityTypeName);
  356. // Note: array_flip will ensure requested bundles are only asked once.
  357. $flippedBundles = array_flip($bundles);
  358. $validFlippedBundles = array_intersect_key($flippedBundles, $bundleInfo);
  359. $uniqueValidBundles = array_flip($validFlippedBundles);
  360. sort($uniqueValidBundles);
  361. if ($bundles !== $uniqueValidBundles) {
  362. throw new \InvalidArgumentException(
  363. "\nRequested bundles: " . implode(', ', $bundles)
  364. . "\nValid bundles: " . implode(', ', $uniqueValidBundles) . "\n");
  365. }
  366. if (empty($uniqueValidBundles)) {
  367. $uniqueValidBundles = array_keys($bundleInfo);
  368. }
  369. return $uniqueValidBundles;
  370. }
  371. }