Browse Source

Refactored Dumper for extensibility. Add term::parent support.

Frederic G. MARAND 7 years ago
parent
commit
63d6f22d0a

+ 15 - 0
reinstall.services.yml

@@ -10,3 +10,18 @@ services:
       - '@entity_type.manager'
       - '@app.root'
       - '@serializer'
+      - '@event_dispatcher'
+      - '%reinstall.path%'
+
+  reinstall.post_dump.file:
+    class: 'Drupal\reinstall\EventSubscriber\FilePostDump'
+    arguments:
+      - '%reinstall.path%'
+    tags:
+      - { name: 'event_subscriber' }
+
+  reinstall.pre_dump.taxonomy_term:
+    class: 'Drupal\reinstall\EventSubscriber\TermPreDump'
+    tags:
+      - { name: 'event_subscriber' }
+

+ 63 - 82
src/Dumper.php

@@ -10,11 +10,14 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Session\AccountSwitcherInterface;
 use Drupal\Core\Session\UserSession;
 use Drupal\file\Entity\File;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Serializer\Serializer;
 use Symfony\Component\Yaml\Yaml;
 
 /**
  * Class Dumper provides export for content entities.
+ *
+ * @see \Drupal\reinstall\DumperEvents
  */
 class Dumper {
 
@@ -52,6 +55,13 @@ class Dumper {
    */
   protected $entityTypeManager;
 
+  /**
+   * The event_dispatcher service.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
   /**
    * The actual path from which to perform imports.
    *
@@ -88,6 +98,8 @@ class Dumper {
    *   The value of the app.root "parameter service".
    * @param \Symfony\Component\Serializer\Serializer $serializer
    *   The serializer service.
+   * @param string $path
+   *   The import path.
    */
   public function __construct(
     AccountSwitcherInterface $accountSwitcher,
@@ -95,15 +107,17 @@ class Dumper {
     EntityTypeManagerInterface $entityTypeManager,
     string $root,
     Serializer $serializer,
-    string $importPath
+    EventDispatcherInterface $eventDispatcher,
+    string $path
     ) {
     $this->accountSwitcher = $accountSwitcher;
     $this->entityTypeBundleInfo = $bundleInfo;
     $this->entityTypeManager = $entityTypeManager;
     $this->root = $root;
     $this->serializer = $serializer;
+    $this->eventDispatcher = $eventDispatcher;
 
-    $this->setPath($importPath);
+    $this->setPath($path);
   }
 
   /**
@@ -180,10 +194,15 @@ class Dumper {
         : [$entityTypeName => $storage->loadMultiple()];
 
       foreach ($entities as $bundleName => $bundleEntities) {
+        // Allow adding data to entities before exporting, like term parents.
+        $eventPre = new DumperEvent($storage, $bundleName, $bundleEntities);
+        $this->eventDispatcher->dispatch(DumperEvents::PRE_DUMP, $eventPre);
+
         $this->dumpEntities($bundleEntities, $entityTypeName, $bundleName);
-        if ($entityTypeName === 'file') {
-          $this->dumpFiles($bundleEntities, $bundleName);
-        }
+
+        // Allow extra work after exporting, like copying files.
+        $eventPost = new DumperEvent($storage, $bundleName, $bundleEntities);
+        $this->eventDispatcher->dispatch(DumperEvents::POST_DUMP, $eventPost);
       }
     }
     $this->accountSwitcher->switchBack();
@@ -194,99 +213,46 @@ class Dumper {
    *
    * @param array $entities
    *   The entities.
-   * @param string $entity_type
+   * @param string $entityTypeName
    *   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);
+  public function dumpEntities(array $entities, string $entityTypeName, string $specifiedBundle = NULL) {
+    $bundle = $specifiedBundle ?: $entityTypeName;
+    $array = $this->toArray($entities);
+    $path = $this->prepareDestination($entityTypeName, $bundle);
+    file_put_contents($path, Yaml::dump($array, static::INLINE_DEPTH, 2));
+  }
 
+  /**
+   * Prepare the dump destination directory and return the file name within it.
+   *
+   * @param string $entityTypeName
+   * @param string $bundleName
+   *
+   * @return string
+   */
+  protected function prepareDestination(string $entityTypeName, string $bundleName): string {
     $importPath = $this->importPath;
-    $dir = "$importPath/$entity_type";
+    $dir = "$importPath/$entityTypeName";
     if (!file_exists($dir)) {
       mkdir($dir, 0777, TRUE);
     }
 
-    $path = "${dir}/${bundle}.yml";
-
-    file_put_contents($path, Yaml::dump($hash, static::INLINE_DEPTH, 2));
+    $path = "${dir}/${bundleName}.yml";
+    return $path;
   }
 
-  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) {
-      // XXX Consider using \0 to support xargs: file names MAY contain spaces.
-      $path = "$dir/$ns.list.txt";
-
-      $nsDir = "$dir/$ns";
-      if (!is_dir($nsDir)) {
-        echo "Creating $nsDir\n";
-        mkdir($nsDir, 0777, TRUE);
-      }
-
-      // fopen() is in text mode by default.
-      $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
+   * Store the absolute path to the import directory.
    *
-   * @return string[]
+   * @param string $path
+   *   The Drupal-root-relative import path.
    *
-   * @see \Drupal\reinstall\Dumper::dumpFiles()
+   * @throws \InvalidArgumentException
+   *   If the directory does not exist.
    */
-  protected function namespacesReducer(array $accu, File $fileItem) {
-    $uri = $fileItem->getFileUri();
-    // Plain filenames without a namespace. Should not happen, but...
-    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);
@@ -298,4 +264,19 @@ class Dumper {
 
     $this->importPath = $real;
   }
+
+  /**
+   * Like NormalizerInterface::normalize(), but for an array.
+   *
+   * @param array $entities
+   *
+   * @return mixed
+   */
+  protected function toArray(array $entities): array {
+    $json_options = [];
+    $json = $this->serializer->serialize($entities, 'json', $json_options);
+    $hash = json_decode($json, TRUE);
+
+    return $hash;
+  }
 }

+ 45 - 0
src/DumperEvent.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\reinstall;
+
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Wraps a dump event for event listeners.
+ *
+ * @see \Drupal\reinstall\DumperEvents::REINSTALL_SERIALIZE_POST
+ * @see \Drupal\reinstall\DumperEvents::SERIALIZE_PRE
+ */
+class DumperEvent extends Event {
+
+  /**
+   * @var string
+   */
+  public $bundleName;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityInterface[]
+   */
+  public $entities = [];
+
+  /**
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  public $storage;
+
+  /**
+   * DumperEvent constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   * @param string $bundleName
+   * @param array $entities
+   */
+  public function __construct(EntityStorageInterface $storage, string $bundleName, array $entities) {
+    $this->storage = $storage;
+    $this->bundleName = $bundleName;
+    $this->entities = $entities;
+  }
+
+}

+ 52 - 0
src/DumperEvents.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\reinstall;
+
+/**
+ * Defines events for the Dumper system.
+ *
+ * @see \Drupal\reinstall\Dumper
+ */
+final class DumperEvents {
+
+  /**
+   * Name of the event fired when entities have been dumped.
+   *
+   * This event allows modules to perform an action whenever content entities
+   * of a given entity type bundle are exported, usually to perform extra
+   * operations, like:
+   * - copying/moving the actual files on file export
+   *
+   * The event listener method receives a \Drupal\reinstall\SerializeEvent
+   * instance, carrying an array of all entites indexed by $entity->id().
+   *
+   * Note that this assumes entity types with an ID, which is not stricly
+   * required by the Entity API, but common enough for most needs.
+   *
+   * @Event
+   *
+   * @var string
+   */
+  const POST_DUMP = 'reinstall.dump.post';
+
+  /**
+   * Name of the event fired when readying entities for dump.
+   *
+   * This event allows modules to perform an action whenever content entities
+   * of a given entity type bundle are exported, usually to add data to the
+   * export, like:
+   * - parent on taxonomy_term
+   * - path on any entity with a canonical path
+   *
+   * The event listener method receives a \Drupal\reinstall\DumperEvent
+   * instance, carrying an array of all entites indexed by $entity->id().
+   *
+   * Note that this assumes entity types with an ID, which is not stricly
+   * required by the Entity API, but common enough for most needs.
+   *
+   * @Event
+   *
+   * @var string
+   */
+  const PRE_DUMP = 'reinstall.dump.pre';
+}

+ 102 - 0
src/EventSubscriber/FilePostDump.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\reinstall\EventSubscriber;
+
+
+use Drupal\reinstall\Dumper;
+use Drupal\reinstall\DumperEvent;
+use Drupal\reinstall\DumperEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class FilePostDump implements EventSubscriberInterface {
+
+  protected $importPath = Dumper::IMPORT_PATH;
+
+  public function __construct(string $importPath) {
+    $this->importPath = $importPath;
+  }
+
+  public static function getSubscribedEvents() {
+    return [
+      DumperEvents::POST_DUMP => 'onDumpPost',
+    ];
+  }
+
+  public function onDumpPost(DumperEvent $event) {
+    $args = func_get_args();
+    if ($event->storage->getEntityTypeId() !== 'file') {
+      return;
+    }
+
+    echo "coucou" . $event->bundleName . "\n";
+    static::dumpFiles($event->bundleName, $event->entities);
+  }
+
+  public function dumpFiles(string $bundle, array $files) {
+    $importPath = $this->importPath;
+    $dir = "$importPath/file";
+    $usedNamespaces = array_keys(array_reduce($files, [__CLASS__, 'namespacesReducer'], []));
+    $lists = [];
+
+    foreach ($usedNamespaces as $ns) {
+      // XXX Consider using \0 to support xargs: file names MAY contain spaces.
+      $path = "$dir/$ns.list.txt";
+
+      $nsDir = "$dir/$ns";
+      if (!is_dir($nsDir)) {
+        echo "Creating $nsDir\n";
+        mkdir($nsDir, 0777, TRUE);
+      }
+
+      // fopen() is in text mode by default.
+      $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 static function namespacesReducer(array $accu, File $fileItem) {
+    $uri = $fileItem->getFileUri();
+    // Plain filenames without a namespace. Should not happen, but...
+    if (FALSE === ($len = Unicode::strpos($uri, '://'))) {
+      return $accu;
+    };
+
+    $namespace = Unicode::substr($uri, 0, $len);
+    $accu[$namespace] = TRUE;
+    return $accu;
+  }
+}

+ 34 - 0
src/EventSubscriber/TermPreDump.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\reinstall\EventSubscriber;
+
+use Drupal\reinstall\DumperEvent;
+use Drupal\reinstall\DumperEvents;
+use Drupal\taxonomy\TermInterface;
+use Drupal\taxonomy\TermStorageInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class TermPreDump implements EventSubscriberInterface {
+
+  public static function getSubscribedEvents() {
+    return [
+      DumperEvents::PRE_DUMP => 'onDumpPre',
+    ];
+  }
+
+  protected static function setParents(TermInterface $term, $tid, TermStorageInterface $storage) {
+    $parents = $storage->loadParents($term->id());
+    if (!empty($parents)) {
+      $term->set('parent', array_keys($parents));
+    }
+  }
+
+  public static function onDumpPre(DumperEvent $event) {
+    $storage = $event->storage;
+    if ($storage->getEntityTypeId() !== 'taxonomy_term') {
+      return;
+    }
+
+    array_walk($event->entities, [__CLASS__, 'setParents'], $storage);
+  }
+}

+ 69 - 0
src/Plugin/migrate/source/ReinstallFileSource.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\reinstall\Plugin\migrate\source;
+
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Row;
+
+/**
+ * Source plugin for terms from a YAML file.
+ *
+ * @MigrateSource(
+ *   id = "reinstall_files"
+ * )
+ */
+class ReinstallFileSource extends SimpleSource {
+
+  /**
+   * Constructor.
+   */
+  public function __construct(
+    array $configuration,
+    string $pluginId,
+    array $pluginDefinition,
+    MigrationInterface $migration = NULL
+  ) {
+    parent::__construct($configuration, $pluginId, $pluginDefinition, $migration);
+    $this->records = array_map([$this, 'flattenRecord'], $this->initialParse($configuration));
+  }
+
+  protected function flattenRecord($record) {
+    $row = new Row($record);
+    $this->flattenRow($row);
+    return $row->getSource();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fields() {
+    $ret = [
+      'fid'               => 'File ID',
+      'uid'               => 'User ID',
+      'uuid'              => 'UUID',
+      'langcode'          => 'ISO 639 language code',
+      'filename'          => 'File name without directory',
+      'uri'               => 'File URI, like public://foo.png',
+      'filemime'          => 'Mime type per https://www.iana.org/assignments/media-types/media-types.xhtml',
+      'filesize'          => 'File size, in bytes',
+      'status'            => 'File status (temporary or permanent)',
+      'created'           => 'Creation timestamp',
+      'changed'           => 'Modification timestamp',
+    ];
+
+    return $ret;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids = [
+      'fid' => [
+        'type' => 'integer',
+      ],
+    ];
+    return $ids;
+  }
+
+}