diff --git a/composer.json b/composer.json index f8dedcb0ea9c467d1d513562f2fa8b2a8baeec06..93a20161e4858dd2a9c261a84dbbaf6a8153f997 100644 --- a/composer.json +++ b/composer.json @@ -1,19 +1,19 @@ { "name": "drupal/migrate_visualize", - "description": "Visualize Drupal Migrations", "type": "drupal-module", + "description": "Visualize Drupal Migrations", "license": "GPL-2.0-or-later", + "require": { + "clue/graph": "^0.9.3", + "graphp/graphviz": "^0.2.2", + "drupal/migrate_plus": "^5.2 || ^6.0" + }, "authors": [ { "name": "Chris Burgess", "email": "chris.burgess@catalyst.net.nz" } ], - "require": { - "clue/graph": "^0.9.3", - "graphp/graphviz": "^0.2.2", - "drupal/migrate_plus": "^5.2 || ^6.0" - }, "support": { "issues": "https://www.drupal.org/project/issues/migrate_visualize", "slack": "#migrate", diff --git a/migrate_visualize.routing.yml b/migrate_visualize.routing.yml index 0d5308b5a9e5fdb637fa1fdf4514fa24eb868433..b48ea6c0b73ed765212ce66adaea142cb6aebd31 100644 --- a/migrate_visualize.routing.yml +++ b/migrate_visualize.routing.yml @@ -21,3 +21,7 @@ migrate_visualize.visualize: _controller: '\Drupal\migrate_visualize\Controller\VisualizeController::build' requirements: _permission: 'access migrate_visualize' + options: + parameters: + migration: + type: entity:migration diff --git a/migrate_visualize.theme.inc b/migrate_visualize.theme.inc index 798b01e058accb66612de068758161d0422eb8ed..3e737dba8f57f6a6f7cccd3835837520dc2d62c8 100644 --- a/migrate_visualize.theme.inc +++ b/migrate_visualize.theme.inc @@ -26,6 +26,7 @@ function _migrate_visualize_graph_type_attributes() { 'process:pipeline' => ['color' => 1, 'shape' => 'box'], 'process:meta' => ['color' => 2, 'shape' => 'box'], 'process:pipeline:step' => ['color' => 3, 'shape' => 'box'], + 'process:pipeline' => ['color' => 4, 'shape' => 'box'], 'plugin' => ['color' => 'cornflowerblue2', 'shape' => 'box'], 'destination' => ['color' => 'darkgoldenrod3', 'shape' => 'box'], ]; @@ -43,19 +44,6 @@ function template_preprocess_migration_visualize_graphviz(array &$variables) { /** @var \Fhaculty\Graph\Graph $graph */ $graph = $variables['graph']; - // Base edge colors on origin vertex type. - $edgeTypes = [ - 'source' => ['colorscheme' => 'bupu9'], - 'source:field' => ['colorscheme' => 'bupu9'], - 'process' => ['colorscheme' => 'bupu9'], - 'process:field' => ['colorscheme' => 'bupu9'], - 'process:pipeline:step' => ['colorscheme' => 'bupu9'], - 'plugin' => ['colorscheme' => 'bupu9'], - 'process:plugin' => ['colorscheme' => 'bupu9'], - 'destination' => ['color' => 'goldenrod3'], - 'destination:field' => ['color' => 'goldenrod3'], - ]; - $graph->setAttribute('graphviz.graph.charset', 'iso-8859-1'); $graph->setAttribute('graphviz.graph.rankdir', 'LR'); @@ -151,5 +139,37 @@ function template_preprocess_migration_visualize_mermaid(array &$variables) { $vertex->setGroup($groups[$groupId]); } } + + $vertexType = $vertex->getAttribute('migrate_visualize.type'); } + + // Base edge colors on origin vertex type. + $edgeTypes = [ + 'source' => ['colorscheme' => 'bupu9'], + 'source:field' => ['colorscheme' => 'bupu9'], + 'process' => ['colorscheme' => 'bupu9'], + 'process:field' => ['colorscheme' => 'bupu9'], + 'process:pipeline:step' => ['colorscheme' => 'bupu9'], + 'plugin' => ['colorscheme' => 'bupu9'], + 'process:plugin' => ['colorscheme' => 'bupu9'], + 'destination' => ['color' => 'goldenrod3'], + 'destination:field' => ['color' => 'goldenrod3'], + ]; + + /** @var \Fhaculty\Graph\Edge\Directed $edge */ + foreach ($graph->getEdges() as $k => $edge) { + $edge->setAttribute('graphviz.colorscheme', 'oranges9'); + $edge->setAttribute('graphviz.color', $k % 6 + 2); + $edge->setAttribute('graphviz.penwidth', '2'); + if ($label = $edge->getAttribute('migrate_visualize.label')) { + $edge->setAttribute('graphviz.label', $label); + } + $edgeType = $edge->getVertexStart()->getAttribute('migrate_visualize.type'); + if (isset($edgeTypes[$edgeType])) { + foreach ($edgeTypes[$edgeType] as $attributeKey => $attributeValue) { + $edge->setAttribute("graphviz.{$attributeKey}", $attributeValue); + } + } + } + } diff --git a/src/Controller/VisualizeController.php b/src/Controller/VisualizeController.php index ac1622a0ff6aa0978e9cb9d6c31bd45b40fe6e65..f99e5206187f021e0b2525175a172c9481cce59d 100644 --- a/src/Controller/VisualizeController.php +++ b/src/Controller/VisualizeController.php @@ -4,8 +4,8 @@ namespace Drupal\migrate_visualize\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; +use Drupal\migrate_plus\Entity\Migration; use Drupal\migrate_visualize\MigrateGraph; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -52,25 +52,15 @@ class VisualizeController extends ControllerBase { /** * Visualize the migration. - * - * @todo Can the parameter be upcast? - * @see https://www.drupal.org/docs/8/api/routing-system/parameters-in-routes/how-upcasting-parameters-works */ - public function build(string $migration) { + public function build(Migration $migration) { + $migrationPlugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $build['switcher'] = $this->formBuilder() ->getForm('Drupal\migrate_visualize\Form\VisualizeMigrationSwitcherForm'); - $migrationInstance = $this->migrationPluginManager->createInstance($migration); - if ($migration !== '' && $migrationInstance === FALSE) { - $this->messenger() - ->addError($this->t('Migration not found: @migration', [ - '@migration' => $migration, - ])); - throw new NotFoundHttpException(); - } - try { - $this->migrationGraph->graph($migrationInstance); + $this->migrationGraph->graph($migrationPlugin); } catch (\Exception $exception) { $this->messenger() @@ -83,7 +73,7 @@ class VisualizeController extends ControllerBase { } $build['visualize'] = [ - '#migration' => $migrationInstance, + '#migration' => $migrationPlugin, '#weight' => 110, ]; diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index 0b8cf793dfddc1ca57f27ec742e78525b83a5d4e..5091edfd56c1e52b3cef0527ec0fa4bea2782b7a 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -4,7 +4,6 @@ namespace Drupal\migrate_visualize\Form; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; -use Graphp\GraphViz\GraphViz; use Fhaculty\Graph\Graph; use Fhaculty\Graph\Exception\UnexpectedValueException; @@ -48,11 +47,15 @@ class SettingsForm extends ConfigFormBase { ]; try { + // Crude, but throws an exception. + if (!class_exists('Graphp\GraphViz\GraphViz')) { + throw new \Exception('Class not found: Graphp\GraphViz\GraphViz'); + } // Test if GraphViz works in this environment. - $graphViz = new GraphViz(); + $graphViz = new \Graphp\GraphViz\GraphViz(); $graphViz->createImageHTML(new Graph()); } - catch (UnexpectedValueException $exception) { + catch (\Exception $exception) { // GraphViz doesn't work in this environment. $form['display_mode']['graphviz']['#disabled'] = TRUE; $form['display_mode']['graphviz']['#description'] .= ' ' . $this->t('Unavailable - see <a href="@docs">documentation</a>.', [ diff --git a/src/Form/VisualizeMigrationSwitcherForm.php b/src/Form/VisualizeMigrationSwitcherForm.php index 9fb6bc968e47b98f04df71a75b673572f0177284..0f5e0c79ddce13974d43b5716ff9545a51e76933 100644 --- a/src/Form/VisualizeMigrationSwitcherForm.php +++ b/src/Form/VisualizeMigrationSwitcherForm.php @@ -101,7 +101,7 @@ class VisualizeMigrationSwitcherForm extends FormBase { ]; if ($current = $this->currentRouteMatch->getParameter('migration')) { - $form['migration']['#default_value'] = $current; + $form['migration']['#default_value'] = $current->id(); } $form['actions']['#type'] = 'actions'; diff --git a/src/MigrateGraph.php b/src/MigrateGraph.php index 911bd7abaa99d749ee46dac3438cf7a22d6649d4..a357aae5a6a0a2be72681932e394222b4d550ea0 100644 --- a/src/MigrateGraph.php +++ b/src/MigrateGraph.php @@ -81,12 +81,26 @@ class MigrateGraph { return $this->graph; } + /** + * Make graph IDs "safe" (for MermaidJS). + * + * @param $id + * + * @return array|string|string[]|null + */ + public function safeId($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 @@ -136,7 +150,8 @@ class MigrateGraph { // Gather source plugin meta values. foreach (['plugin'] as $configKey) { if (isset($configuration[$configKey]) && !$graph->hasVertex("source:meta:{$configKey}")) { - $metaVertex = $graph->createVertex("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", [ @@ -149,7 +164,7 @@ class MigrateGraph { switch ($configuration['plugin']) { case 'embedded_data': foreach (reset($configuration['data_rows']) as $key => $value) { - $fieldVertexId = "source:field:{$key}"; + $fieldVertexId = $this->safeId("source:field:{$key}"); if (!$graph->hasVertex($fieldVertexId)) { $fieldVertex = $graph->createVertex($fieldVertexId); $fieldVertex->setAttribute('migrate_visualize.discovery', 'source'); @@ -168,7 +183,7 @@ class MigrateGraph { // (eg JSON, XML, CSV sources). if (isset($configuration['fields'])) { foreach ($configuration['fields'] as $field) { - $fieldVertexId = "source:field:{$field['name']}"; + $fieldVertexId = $this->safeId("source:field:{$field['name']}"); if (!$graph->hasVertex($fieldVertexId)) { $fieldVertex = $graph->createVertex($fieldVertexId); $fieldVertex->setAttribute('migrate_visualize.discovery', 'source'); @@ -193,7 +208,8 @@ class MigrateGraph { // Gather destination plugin meta values. foreach (['plugin', 'default_bundle'] as $configKey) { if (isset($configuration[$configKey]) && !$graph->hasVertex("destination:meta:{$configKey}")) { - $metaVertex = $graph->createVertex("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", [ @@ -214,7 +230,8 @@ class MigrateGraph { $entityType = explode(':', $configuration['plugin'])[1]; $fields = $this->entityFieldManager->getFieldDefinitions($entityType, $configuration['default_bundle']); foreach ($fields as $fieldId => $field) { - $fieldVertex = $graph->createVertex("destination:field:{$fieldId}"); + $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", [ @@ -236,7 +253,7 @@ class MigrateGraph { $graph = $this->getGraph(); /** @var \Drupal\migrate\Plugin\MigrateProcessInterface[] $plugins */ foreach ($pipeline as $destinationPropertyName => $plugins) { - $processVertexId = "process:field:{$destinationPropertyName}"; + $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", [ @@ -248,7 +265,7 @@ class MigrateGraph { $this->graphProcessPlugin($destinationPropertyName, $plugin, $processVertex, $pipelineStepId, $plugins); } - $destinationVertexId = "destination:field:{$destinationPropertyName}"; + $destinationVertexId = $this->safeid("destination:field:{$destinationPropertyName}"); if ($graph->hasVertex($destinationVertexId)) { $destinationVertex = $graph->getVertex($destinationVertexId); @@ -292,7 +309,7 @@ class MigrateGraph { return; } - $pluginVertexId = "process:field:{$destinationPropertyName}:{$pipelineStepId}"; + $pluginVertexId = $this->safeId("process:field:{$destinationPropertyName}:{$pipelineStepId}"); $pluginVertex = $graph->createVertex($pluginVertexId); $pluginVertex->setAttribute('migrate_visualize.type', 'process:pipeline:step'); @@ -313,7 +330,7 @@ class MigrateGraph { } foreach ($sources as $sourceField) { - $sourceVertexId = "source:field:{$sourceField}"; + $sourceVertexId = $this->safeId("source:field:{$sourceField}"); if ($graph->hasVertex($sourceVertexId)) { $sourceVertex = $graph->getVertex($sourceVertexId); } @@ -340,7 +357,7 @@ class MigrateGraph { if (count($plugins) == ($pipelineStepId + 1)) { $stepPipelineEdge = $pluginVertex->createEdgeTo($destinationVertex); - if ($graph->hasVertex($sourceVertexId)) { + if (isset($sourceVertexId) && $graph->hasVertex($sourceVertexId)) { if (!$sourceVertex->hasEdgeTo($pluginVertex)) { $sourceStepEdge = $sourceVertex->createEdgeTo($pluginVertex); } diff --git a/tests/src/Functional/VisualizeControllerTest.php b/tests/src/Functional/VisualizeControllerTest.php index e42b494afa88fa6cdf00e863961528e01c9a91ad..1a5780fe15647905dc2971dbfe435910b81373e0 100644 --- a/tests/src/Functional/VisualizeControllerTest.php +++ b/tests/src/Functional/VisualizeControllerTest.php @@ -92,7 +92,7 @@ class VisualizeControllerTest extends BrowserTestBase { public static function visualizeControllerProvider(): array { $cases = []; $cases[] = ['fruit_terms', 200, 'Fruit Terms']; - $cases[] = ['does_not_exist', 404, 'Migration not found']; + $cases[] = ['does_not_exist', 404, 'Page not found']; /* $cases[] = ['broken', 200, 'Unable to graph migration']; */ return $cases; }