migrate_visualize-1.0.x-dev/src/MigrateGraph.php

src/MigrateGraph.php
<?php

namespace Drupal\migrate_visualize;

use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Fhaculty\Graph\Graph;
use Fhaculty\Graph\Vertex;

/**
 * Inspect a Drupal Migration and capture analysis in a graph.
 */
class MigrateGraph {

  use StringTranslationTrait;

  /**
   * Module configuration.
   *
   * @var Drupal\Core\Config\ImmutableConfig
   */
  protected ImmutableConfig $config;

  /**
   * The migration to be graphed.
   *
   * @var \Drupal\migrate\Plugin\MigrationInterface
   */
  protected MigrationInterface $migration;

  /**
   * Graph which captures our analysis of the Migration.
   *
   * @var \Fhaculty\Graph\Graph
   */
  protected Graph $graph;

  /**
   * Process plugin keys which denote a source being referred to.
   *
   * @var string[]
   */
  protected $sourceKeys = [
    'source',
  ];

  /**
   * Construct a new MigrationGraph object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
   *   The entity field manager.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
   *   The string translation service.
   */
  public function __construct(
    protected ConfigFactoryInterface $configFactory,
    protected EntityFieldManagerInterface $entityFieldManager,
    protected $stringTranslation,
  ) {
    $this->config = $this->configFactory->get('migrate_visualize.settings');
    $this->graph = new Graph();
  }

  /**
   * Set the migration to be graphed.
   *
   * @var \Drupal\migrate\Plugin\MigrationInterface
   *   The migration to graph.
   */
  public function setMigration(MigrationInterface $migration) {
    $this->migration = $migration;
  }

  /**
   * Retrieve the analysis.
   *
   * @return \Fhaculty\Graph\Graph
   *   The generated graph.
   */
  public function getGraph() {
    return $this->graph;
  }

  /**
   * Make graph IDs "safe" (for MermaidJS).
   *
   * @param string $id
   *   ID (yaml key) of a graph item.
   *
   * @return string
   *   ID, safe for use with MermaidJS.
   */
  public function safeId(string $id) {
    return preg_replace('/[^a-zA-Z0-9_:]/', '_', $id);
  }

  /**
   * Bypass PHP protections on object properties for analysis.
   *
   * Getting at process plugin configs seems complicated, but I'm pretty sure
   * this ain't how it's supposed to happen.
   *
   * Hmm ... can we use $migration->toArray(), if we have the migration config
   * entity, instead of picking the migration plugin object apart?
   *
   * @var object $object
   *   The object to inspect.
   * @var string $propertyName
   *   The name of the property to retrieve.
   *
   * @return mixed
   *   The property value extracted.
   *
   * @see https://www.drupal.org/project/drupal/issues/2937177
   * @see https://www.drupal.org/project/feeds_migrate/issues/3012966
   */
  public static function extractProtectedProperty($object, $propertyName) {
    $extractor = function () use ($propertyName) {
      // @phpstan-ignore variable.undefined
      return $this->$propertyName;
    };
    return $extractor->call($object);
  }

  /**
   * Graph a migration.
   *
   * @param \Drupal\migrate\Plugin\MigrationInterface $migration
   *   The migration under inspection.
   *
   * @throws \Exception
   */
  public function graph(MigrationInterface $migration) : void {
    $this->setMigration($migration);

    $this->graphSourcePlugin($this->migration->getSourcePlugin(), $this->migration->getSourceConfiguration());
    $this->graphDestinationPlugin($this->migration->getDestinationPlugin(), $this->migration->getDestinationConfiguration());
    $this->graphProcessPlugins($this->migration->getProcessPlugins());
  }

  /**
   * Graph the source plugin.
   *
   * The source plugin fields often aren't complete, additional fields will be
   * inferred from process as well.
   *
   * NB: $sourcePlugin->getFields() exists, but may return labels if present,
   * and we want unique field names.
   */
  protected function graphSourcePlugin($sourcePlugin, array $configuration) : void {
    $graph = $this->getGraph();

    // Gather source plugin meta values.
    foreach (['plugin'] as $configKey) {
      if (isset($configuration[$configKey]) && !$graph->hasVertex("source:meta:{$configKey}")) {
        $metaVertexId = $this->safeId("source:meta:{$configKey}");
        $metaVertex = $graph->createVertex($metaVertexId);
        $metaVertex->setAttribute('migrate_visualize.discovery', 'source');
        $metaVertex->setAttribute('migrate_visualize.type', 'source:meta');
        $metaVertex->setAttribute('migrate_visualize.label', $this->t("@key: @value", [
          '@key' => $configKey,
          '@value' => $configuration[$configKey],
        ]));
      }
    }

    switch ($configuration['plugin']) {
      case 'embedded_data':
        foreach (reset($configuration['data_rows']) as $key => $value) {
          $fieldVertexId = $this->safeId("source:field:{$key}");
          if (!$graph->hasVertex($fieldVertexId)) {
            $fieldVertex = $graph->createVertex($fieldVertexId);
            $fieldVertex->setAttribute('migrate_visualize.discovery', 'source');
          }
          else {
            $fieldVertex = $graph->getVertex($fieldVertexId);
          }
          $fieldVertex->setAttribute('migrate_visualize.type', 'source:field');
          $fieldVertex->setAttribute('migrate_visualize.key', "{$fieldVertexId}");
          $fieldVertex->setAttribute('migrate_visualize.label', "{$key}");
        }
        break;
    }

    // Gather source plugin fields when the source plugin defines fields
    // (eg JSON, XML, CSV sources).
    if (isset($configuration['fields'])) {
      foreach ($configuration['fields'] as $field) {
        $fieldVertexId = $this->safeId("source:field:{$field['name']}");
        if (!$graph->hasVertex($fieldVertexId)) {
          $fieldVertex = $graph->createVertex($fieldVertexId);
          $fieldVertex->setAttribute('migrate_visualize.discovery', 'source');
        }
        else {
          $fieldVertex = $graph->getVertex($fieldVertexId);
        }
        $fieldVertex->setAttribute('migrate_visualize.type', 'source:field');
        $fieldVertex->setAttribute('migrate_visualize.key', "{$fieldVertexId}");
        $fieldVertex->setAttribute('migrate_visualize.label', "{$field['name']}");
      }
    }
  }

  /**
   * Graph the destination plugin.
   */
  protected function graphDestinationPlugin($destinationPlugin, array $configuration) : void {
    $graph = $this->getGraph();
    $configuration = self::extractProtectedProperty($this->migration->getDestinationPlugin(), 'configuration');

    // Gather destination plugin meta values.
    foreach (['plugin', 'default_bundle'] as $configKey) {
      if (isset($configuration[$configKey]) && !$graph->hasVertex("destination:meta:{$configKey}")) {
        $metaVertexId = $this->safeId("destination:meta:{$configKey}");
        $metaVertex = $graph->createVertex($metaVertexId);
        $metaVertex->setAttribute('migrate_visualize.discovery', 'destination');
        $metaVertex->setAttribute('migrate_visualize.type', 'destination:meta');
        $metaVertex->setAttribute('migrate_visualize.label', $this->t("@key: @value", [
          '@key' => $configKey,
          '@value' => $configuration[$configKey],
        ]));
        $metaVertex->setAttribute('configuration', $configuration);
      }
    }

    // @todo Check better if it starts with "entity:".
    $entityPlugins = [
      'entity',
      'entity_complete',
    ];
    if (in_array(explode(':', $configuration['plugin'])[0], $entityPlugins)) {
      if (isset($configuration['default_bundle'])) {
        $entityType = explode(':', $configuration['plugin'])[1];
        $fields = $this->entityFieldManager->getFieldDefinitions($entityType, $configuration['default_bundle']);
        foreach ($fields as $fieldId => $field) {
          $fieldVertexId = $this->safeId("destination:field:{$fieldId}");
          $fieldVertex = $graph->createVertex($fieldVertexId);
          $fieldVertex->setAttribute('migrate_visualize.discovery', 'destination');
          $fieldVertex->setAttribute('migrate_visualize.type', 'destination:field');
          $fieldVertex->setAttribute('migrate_visualize.label', $this->t("@fieldId: @value", [
            '@fieldId' => $fieldId,
            '@value' => $field->getLabel(),
          ]));
        }
      }
    }
  }

  /**
   * Graph the process plugin pipelines.
   *
   * @var array $processPlugins
   *   The process plugin pipelines for this migration.
   */
  protected function graphProcessPlugins($processPlugins) : void {
    $graph = $this->getGraph();
    /** @var \Drupal\migrate\Plugin\MigrateProcessInterface[] $plugins */
    foreach ($processPlugins as $destinationPropertyName => $plugins) {
      $processVertexId = $this->safeId("process:field:{$destinationPropertyName}");
      $processVertex = $graph->createVertex($processVertexId);
      $processVertex->setAttribute('migrate_visualize.type', 'process:pipeline');
      $processVertex->setAttribute('migrate_visualize.label', $this->t("@fieldId", [
        '@fieldId' => $destinationPropertyName,
      ]));

      $previousVertex = NULL;
      foreach ($plugins as $pipelineStepId => $plugin) {
        $previousVertex = $this->graphProcessPlugin($destinationPropertyName, $plugin, $processVertex, $pipelineStepId, $plugins, $previousVertex);
      }

      $destinationVertexId = $this->safeid("destination:field:{$destinationPropertyName}");

      if ($graph->hasVertex($destinationVertexId)) {
        // Destination was created from inspecting entity fields.
        $destinationVertex = $graph->getVertex($destinationVertexId);
      }
      else {
        // Dynamically create a destination field.
        $destinationVertex = $graph->createVertex($destinationVertexId);
        $destinationVertex->setAttribute('migrate_visualize.type', 'destination:field');
        $destinationVertex->setAttribute('migrate_visualize.key', "{$destinationVertexId}");
        $destinationVertex->setAttribute('migrate_visualize.label', "{$destinationPropertyName}");
        $destinationVertex->setAttribute('migrate_visualize.discovery', 'process');
      }
      $processVertex->createEdgeTo($destinationVertex);
    }
  }

  /**
   * Graph each plugin (step) in the process.
   *
   * @param \string $destinationPropertyName
   *   The calculated destination property name for this plugin in the process.
   * @param \Drupal\migrate\Plugin\MigrateProcessInterface $plugin
   *   The configured process plugin.
   * @param \Fhaculty\Graph\Vertex $destinationVertex
   *   The vertex associated with this process's destination.
   * @param \int $pipelineStepId
   *   The array ID of this plugin in its pipeline.
   * @param \array $plugins
   *   The array of plugins in this pipeline.
   * @param \Fhaculty\Graph\Vertex $additionalSourceVertex
   *   (Optional) An additional source vertex. Used for chained sub-processes.
   */
  protected function graphProcessPlugin(string $destinationPropertyName, MigrateProcessInterface $plugin, Vertex $destinationVertex, int $pipelineStepId, array $plugins, ?Vertex $additionalSourceVertex = NULL) {
    $graph = $this->getGraph();

    /** @var ProcessPluginBase $processPlugin */
    $configuration = self::extractProtectedProperty($plugin, 'configuration');
    $definition = $plugin->getPluginDefinition();
    $pluginId = $definition['id'];

    // @see https://www.drupal.org/project/migrate_visualize/issues/3262781
    if ($pluginId === 'get' && $configuration['plugin'] !== 'get') {
      return;
    }

    $pluginVertexId = $this->safeId("process:field:{$destinationPropertyName}:{$pipelineStepId}");
    $pluginVertex = $graph->createVertex($pluginVertexId);
    $previousPluginVertex = NULL;

    $pluginVertex->setAttribute('migrate_visualize.type', 'process:pipeline:step');
    $pluginVertex->setAttribute('migrate_visualize.label', $this->getProcessPluginStepLabel($plugin, $configuration));

    // Connect to sources.
    $sourceVertices = [];
    foreach ($this->sourceKeys as $key) {
      if (isset($configuration[$key])) {
        if (is_string($configuration[$key])) {
          $sources = [$configuration[$key]];
        }
        elseif (is_array($configuration[$key])) {
          $sources = $configuration[$key];
        }
        else {
          // @todo What here?
          $sources = ['unknown'];
        }

        foreach ($sources as $sourceField) {
          $sourceVertexId = $this->safeId("source:field:{$sourceField}");

          // For this source, generate matching pseudofield destination ID.
          $prevDestId = preg_replace('#^@([^/]+).*#', '$1', $sourceField);
          $prevDestinationVertexId = $this->safeId("destination:field:{$prevDestId}");

          if ($graph->hasVertex($sourceVertexId)) {
            // Source vertex created in graphSourcePlugin().
            $sourceVertex = $graph->getVertex($sourceVertexId);
          }
          else {
            // Dynamically create source vertex for field.
            $sourceVertex = $graph->createVertex($sourceVertexId);
            $sourceVertex->setAttribute('migrate_visualize.type', 'source:field');
            $sourceVertex->setAttribute('migrate_visualize.key', $sourceVertexId);
            $sourceVertex->setAttribute('migrate_visualize.label', "{$sourceField}");
            $sourceVertex->setAttribute('migrate_visualize.discovery', 'process');
            if ($sourceField[0] === '@' && $this->config->get('graph_pseudofield_sources')) {
              if ($this->graph->hasVertex($prevDestinationVertexId)) {
                $edge = $this->graph->getVertex($prevDestinationVertexId)
                  ->createEdgeTo($sourceVertex);
                $edge->setAttribute('migrate_visualize.discovery', 'process');
                $edge->setAttribute('migrate_visualize.type', 'pseudofield');
              }
            }
          }
          $sourceVertices[] = $sourceVertex;
        }
      }
    }

    // Check if this plugin is part of a chain as have an additional source
    // vertex passed as an argument.
    if ($additionalSourceVertex) {
      $sourceVertices[] = $additionalSourceVertex;
    }
    foreach ($sourceVertices as $sourceVertex) {
      if (!$sourceVertex->hasEdgeTo($pluginVertex)) {
        $edge = $sourceVertex->createEdgeTo($pluginVertex);
      }
    }

    // Previous step exists.
    if (!is_null($previousPluginVertex)) {
      $previousPluginEdge = $previousPluginVertex->createEdgeTo($pluginVertex);
    }
    $previousPluginVertex = $pluginVertex;

    // Last step in pipeline.
    if (count($plugins) == ($pipelineStepId + 1)) {
      $pluginVertex->createEdgeTo($destinationVertex);
    }

    return $pluginVertex;
  }

  /**
   * Generate a label for a process pipeline step.
   *
   * @param \Drupal\migrate\Plugin\MigrateProcessInterface $processPlugin
   *   The configured process plugin.
   * @param \array $configuration
   *   The extracted configuration of the plugin.
   *
   * @return string
   *   The generated label.
   */
  protected function getProcessPluginStepLabel(MigrateProcessInterface $processPlugin, array $configuration) : string {
    switch ($configuration['plugin']) {
      case "callback":
        return "callback: {$configuration['callable']}";

      case "get":
        return "get";

      case "entity_generate":
        return "entity_generate";

      default:
        return $configuration['plugin'];
    }
    return get_class($processPlugin);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc