diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..fa75838
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+end_of_line = LF
+indent_style = space
+indent_size = 4
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[{.phpstan/,phpstan}*.neon]
+indent_style = tab
+
+[*.{sh,bash,zsh}]
+indent_style = tab
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 976068b..b831c69 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,33 +1,34 @@
name: CI
on:
- push:
- pull_request:
+ push: ~
+ pull_request: ~
jobs:
+ lint:
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ - run: composer install
+ - run: vendor/bin/phpcs
+ - run: vendor/bin/psalm
PHPUnit:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
php:
- - 7.4
- - 7.3
- - 7.2
- - 7.1
- - 7.0
- - 5.6
- - 5.5
- - 5.4
- - 5.3
+ - '8.3'
+ - '8.4'
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- run: sudo apt-get -y install graphviz
- run: composer install
- - run: vendor/bin/phpunit --coverage-text
- if: ${{ matrix.php >= 7.3 }}
- - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy
- if: ${{ matrix.php < 7.3 }}
+ - run: vendor/bin/phpunit
diff --git a/.gitignore b/.gitignore
index de4a392..b4679ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,24 @@
+### Common
+/.cache/
+/reports/
+
+
+### Composer
/vendor
/composer.lock
+/graphp-graphviz-*.tar
+/graphp-graphviz-*.tar.gz
+/graphp-graphviz-*.tar.bz2
+/graphp-graphviz-*.zip
+
+
+### PHPCS
+/phpcs.xml
+
+
+### PSalm
+/psalm.neon
+
+
+### PHPUnit
+/phpunit.xml
diff --git a/composer.json b/composer.json
index 96feafc..f81df38 100644
--- a/composer.json
+++ b/composer.json
@@ -2,17 +2,33 @@
"name": "graphp/graphviz",
"type": "library",
"description": "GraphViz graph drawing for the mathematical graph/network library GraPHP.",
- "keywords": ["GraphViz", "graph drawing", "graph image", "dot output", "GraPHP"],
+ "keywords": [
+ "GraphViz",
+ "graph drawing",
+ "graph image",
+ "dot output",
+ "GraPHP"
+ ],
"homepage": "https://github.com/graphp/graphviz",
"license": "MIT",
- "autoload": {
- "psr-4": {"Graphp\\GraphViz\\": "src/"}
- },
"require": {
- "php": ">=5.3.0",
+ "php": ">=8.3",
"graphp/graph": "^1@dev"
},
"require-dev": {
- "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
+ "phpunit/phpunit": "^11.5",
+ "psalm/plugin-phpunit": "^0.19.5",
+ "squizlabs/php_codesniffer": "^3.13",
+ "vimeo/psalm": "^6.13"
+ },
+ "autoload": {
+ "psr-4": {
+ "Graphp\\GraphViz\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Graphp\\GraphViz\\Tests\\": "tests/src/"
+ }
}
}
diff --git a/examples/01-simple.php b/examples/01-simple.php
index 3664e5e..d762d86 100644
--- a/examples/01-simple.php
+++ b/examples/01-simple.php
@@ -1,8 +1,13 @@
createVertex();
$blue->setAttribute('id', 'blue');
@@ -15,5 +20,5 @@
$edge = $graph->createEdgeDirected($blue, $red);
$edge->setAttribute('graphviz.color', 'grey');
-$graphviz = new Graphp\GraphViz\GraphViz();
+$graphviz = new GraphViz();
$graphviz->display($graph);
diff --git a/examples/02-html.php b/examples/02-html.php
index c7ea6d8..0b87229 100644
--- a/examples/02-html.php
+++ b/examples/02-html.php
@@ -1,25 +1,26 @@
setAttribute('graphviz.graph.rankdir', 'LR');
$hello = $graph->createVertex()->setAttribute('id', 'hello');
$world = $graph->createVertex()->setAttribute('id', 'wörld');
$graph->createEdgeDirected($hello, $world);
-$graphviz = new Graphp\GraphViz\GraphViz();
+$graphviz = new GraphViz();
$graphviz->setFormat('svg');
echo '
-
+
hello wörld
-
-' . $graphviz->createImageHtml($graph) . '
-
-
-';
+', $graphviz->createImageHtml($graph), '';
diff --git a/examples/11-uml-html.php b/examples/11-uml-html.php
index bd5011c..d00809e 100644
--- a/examples/11-uml-html.php
+++ b/examples/11-uml-html.php
@@ -1,10 +1,13 @@
createVertex()->setAttribute('id', 'Entity');
$a->setAttribute('graphviz.shape', 'none');
diff --git a/examples/12-uml-records.php b/examples/12-uml-records.php
index 26c5f78..d9a6cdb 100644
--- a/examples/12-uml-records.php
+++ b/examples/12-uml-records.php
@@ -1,10 +1,13 @@
createVertex()->setAttribute('id', 'Entity');
$a->setAttribute('graphviz.shape', 'record');
diff --git a/examples/13-record-ports.php b/examples/13-record-ports.php
index 790f28d..436849b 100644
--- a/examples/13-record-ports.php
+++ b/examples/13-record-ports.php
@@ -1,10 +1,13 @@
createVertex();
$a->setAttribute('graphviz.shape', 'Mrecord');
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..f4fc0bb
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,12 @@
+
+
+
+ ./examples/
+ ./src/
+ ./tests/src/
+
+
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 7e8a89a..a63d96e 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,19 +1,40 @@
+
+
+
+
+ ./src
+
+
-
-
-
- ./tests/
+
+ ./tests/src/Unit/
+
+
+
+
+
+
+
+
-
- ./src
-
+
+
+
+
+
+
+
diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy
deleted file mode 100644
index fac174b..0000000
--- a/phpunit.xml.legacy
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
- ./tests/
-
-
-
-
- ./src
-
-
-
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..292729a
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Dot.php b/src/Dot.php
index 0111db4..8338c98 100644
--- a/src/Dot.php
+++ b/src/Dot.php
@@ -1,14 +1,19 @@
graphviz = $graphviz;
}
- public function getOutput(Graph $graph)
+ public function getOutput(Graph $graph): string
{
return $this->graphviz->createScript($graph);
}
diff --git a/src/GraphViz.php b/src/GraphViz.php
index fcddeb0..f0563f1 100644
--- a/src/GraphViz.php
+++ b/src/GraphViz.php
@@ -1,5 +1,7 @@
executable = $executable;
return $this;
@@ -78,20 +79,20 @@ public function setExecutable($executable) {
/**
* return executable to use
*
- * @return string
* @see GraphViz::setExecutable()
*/
- public function getExecutable() {
+ public function getExecutable(): string
+ {
return $this->executable;
}
/**
* set graph image output format
*
- * @param string $format png, svg, ps2, etc. (see 'man dot' for details on parameter '-T')
- * @return GraphViz $this (chainable)
+ * @param string $format
+ * png, svg, ps2, etc. (see 'man dot' for details on parameter '-T')
*/
- public function setFormat($format)
+ public function setFormat(string $format): static
{
$this->format = $format;
@@ -101,11 +102,9 @@ public function setFormat($format)
/**
* create and display image for this graph
*
- * @param Graph $graph graph to display
- * @return void
* @uses GraphViz::createImageFile()
*/
- public function display(Graph $graph)
+ public function display(Graph $graph): void
{
// echo "Generate picture ...";
$tmp = $this->createImageFile($graph);
@@ -128,34 +127,34 @@ public function display(Graph $graph)
exec('xdg-open ' . escapeshellarg($tmp) . ' > /dev/null 2>&1 &');
}
- $next = microtime(true) + self::DELAY_OPEN;
- // echo "... done\n";
+ $next = microtime(true) + (float) self::DELAY_OPEN;
}
/**
* create image file data contents for this graph
*
- * @param Graph $graph graph to display
- * @return string
+ * @param Graph $graph
+ * graph to display
+ *
* @uses GraphViz::createImageFile()
*/
- public function createImageData(Graph $graph)
+ public function createImageData(Graph $graph): string
{
$file = $this->createImageFile($graph);
$data = file_get_contents($file);
unlink($file);
- return $data;
+ return $data === false ? '' : $data;
}
/**
* create base64-encoded image src target data to be used for html images
*
* @param Graph $graph graph to display
- * @return string
+ *
* @uses GraphViz::createImageData()
*/
- public function createImageSrc(Graph $graph)
+ public function createImageSrc(Graph $graph): string
{
$format = $this->format;
if ($this->format === 'svg' || $this->format === 'svgz') {
@@ -169,10 +168,10 @@ public function createImageSrc(Graph $graph)
* create image html code for this graph
*
* @param Graph $graph graph to display
- * @return string
+ *
* @uses GraphViz::createImageSrc()
*/
- public function createImageHtml(Graph $graph)
+ public function createImageHtml(Graph $graph): string
{
if ($this->format === 'svg' || $this->format === 'svgz') {
return '';
@@ -184,15 +183,19 @@ public function createImageHtml(Graph $graph)
/**
* create image file for this graph
*
- * @param Graph $graph graph to display
- * @return string filename
+ * @param Graph $graph
+ * graph to display
+ *
+ * @return string
+ * filename
+ *
* @throws \UnexpectedValueException on error
+ *
* @uses GraphViz::createScript()
*/
- public function createImageFile(Graph $graph)
+ public function createImageFile(Graph $graph): string
{
$script = $this->createScript($graph);
- // var_dump($script);
$tmp = tempnam(sys_get_temp_dir(), 'graphviz');
if ($tmp === false) {
@@ -207,26 +210,38 @@ public function createImageFile(Graph $graph)
$ret = 0;
$executable = $this->getExecutable();
- system(escapeshellarg($executable) . ' -T ' . escapeshellarg($this->format) . ' ' . escapeshellarg($tmp) . ' -o ' . escapeshellarg($tmp . '.' . $this->format), $ret);
+ $dstFilePath = $tmp . '.' . $this->format;
+ $command = sprintf(
+ "%s -T %s %s -o %s",
+ escapeshellcmd($executable),
+ escapeshellarg($this->format),
+ escapeshellarg($tmp),
+ escapeshellarg($dstFilePath),
+ );
+ system($command, $ret);
+ unlink($tmp);
+
if ($ret !== 0) {
- throw new \UnexpectedValueException('Unable to invoke "' . $executable .'" to create image file (code ' . $ret . ')');
+ throw new \UnexpectedValueException(sprintf(
+ 'Unable to invoke "%s" to create image file (code %d)',
+ $executable,
+ $ret,
+ ));
}
- unlink($tmp);
-
- return $tmp . '.' . $this->format;
+ return $dstFilePath;
}
/**
* create graphviz script representing this graph
*
* @param Graph $graph graph to display
- * @return string
+
* @uses Directed::hasDirected()
* @uses Graph::getVertices()
* @uses Graph::getEdges()
*/
- public function createScript(Graph $graph)
+ public function createScript(Graph $graph): string
{
$hasDirectedEdges = false;
foreach ($graph->getEdges() as $edge) {
@@ -246,10 +261,10 @@ public function createScript(Graph $graph)
$name = $this->escape($name) . ' ';
}
- $script = ($hasDirectedEdges ? 'di':'') . 'graph ' . $name . '{' . self::EOL;
+ $script = ($hasDirectedEdges ? 'di' : '') . 'graph ' . $name . '{' . self::EOL;
// add global attributes
- foreach (array('graph', 'node', 'edge') as $key) {
+ foreach (['graph', 'node', 'edge'] as $key) {
if ($layout = $this->getAttributesPrefixed($graph, 'graphviz.' . $key . '.')) {
$script .= $this->formatIndent . $key . ' ' . $this->escapeAttributes($layout) . self::EOL;
}
@@ -257,11 +272,10 @@ public function createScript(Graph $graph)
// build an array to map vertex hashes to vertex IDs for output
$tid = 0;
- $vids = array();
+ $vids = [];
- $groups = array();
+ $groups = [];
foreach ($graph->getVertices() as $vertex) {
- assert($vertex instanceof Vertex);
$groups[$vertex->getAttribute('group', 0)][] = $vertex;
$id = $vertex->getAttribute('id');
@@ -278,13 +292,19 @@ public function createScript(Graph $graph)
$gid = 0;
// put each group of vertices in a separate subgraph cluster
foreach ($groups as $group => $vertices) {
- $script .= $this->formatIndent . 'subgraph cluster_' . $gid++ . ' {' . self::EOL .
- $indent . 'label = ' . $this->escape($group) . self::EOL;
+ $script .= $this->formatIndent
+ . 'subgraph cluster_'
+ . $gid++
+ . ' {'
+ . self::EOL
+ . $indent . 'label = '
+ . $this->escape((string) $group)
+ . self::EOL;
foreach ($vertices as $vertex) {
$vid = $vids[\spl_object_hash($vertex)];
$layout = $this->getLayoutVertex($vertex, $vid);
- $script .= $indent . $this->escape($vid);
+ $script .= $indent . $this->escape((string) $vid);
if ($layout) {
$script .= ' ' . $this->escapeAttributes($layout);
}
@@ -295,12 +315,12 @@ public function createScript(Graph $graph)
} else {
// explicitly add all isolated vertices (vertices with no edges) and vertices with special layout set
// other vertices wil be added automatically due to below edge definitions
- foreach ($graph->getVertices() as $vertex){
+ foreach ($graph->getVertices() as $vertex) {
$vid = $vids[\spl_object_hash($vertex)];
$layout = $this->getLayoutVertex($vertex, $vid);
if ($layout || !$vertex->getEdges()) {
- $script .= $this->formatIndent . $this->escape($vid);
+ $script .= $this->formatIndent . $this->escape((string) $vid);
if ($layout) {
$script .= ' ' . $this->escapeAttributes($layout);
}
@@ -314,9 +334,12 @@ public function createScript(Graph $graph)
// add all edges as directed edges
foreach ($graph->getEdges() as $edge) {
$vertices = $edge->getVertices();
- assert($vertices[0] instanceof Vertex && $vertices[1] instanceof Vertex);
+ assert(isset($vertices[0]) && isset($vertices[1]));
- $script .= $this->formatIndent . $this->escape($vids[\spl_object_hash($vertices[0])]) . $edgeop . $this->escape($vids[\spl_object_hash($vertices[1])]);
+ $script .= $this->formatIndent
+ . $this->escape((string) $vids[\spl_object_hash($vertices[0])])
+ . $edgeop
+ . $this->escape((string) $vids[\spl_object_hash($vertices[1])]);
$layout = $this->getLayoutEdge($edge);
@@ -338,11 +361,9 @@ public function createScript(Graph $graph)
/**
* escape given string value and wrap in quotes if needed
*
- * @param string $id
- * @return string
* @link http://graphviz.org/content/dot-language
*/
- private function escape($id)
+ private function escape(string $id): string
{
// see @link: There is no semantic difference between abc_2 and "abc_2"
// numeric or simple string, no need to quote (only for simplicity)
@@ -350,35 +371,48 @@ private function escape($id)
return $id;
}
- return '"' . str_replace(array('&', '<', '>', '"', "'", '\\', "\n"), array('&', '<', '>', '"', ''', '\\\\', '\\l'), $id) . '"';
+ $pairs = [
+ '&' => '&',
+ '<' => '<',
+ '>' => '>',
+ '"' => '"',
+ "'" => ''',
+ '\\' => '\\\\',
+ "\n" => '\\l',
+ ];
+
+ return sprintf('"%s"', strtr($id, $pairs));
}
/**
* get escaped attribute string for given array of (unescaped) attributes
*
- * @param array $attrs
- * @return string
+ * @param array $attrs
+ *
* @uses GraphViz::escape()
*/
- private function escapeAttributes($attrs)
+ private function escapeAttributes(array $attrs): string
{
$script = '[';
$first = true;
foreach ($attrs as $name => $value) {
+ settype($name, 'string');
+ settype($value, 'string');
+
if ($first) {
$first = false;
} else {
$script .= ' ';
}
- if (\substr($name, -5) === '_html') {
+ if (str_ends_with($name, '_html')) {
// HTML-like labels need to be wrapped in angle brackets
$name = \substr($name, 0, -5);
$value = '<' . $value . '>';
- } elseif (\substr($name, -7) === '_record') {
+ } elseif (str_ends_with($name, '_record')) {
// record labels need to be quoted
$name = \substr($name, 0, -7);
- $value = '"' . \str_replace('"', '\\"', $value) . '"';
+ $value = '"' . addcslashes($value, '"') . '"';
} else {
// all normal attributes need to be escaped and/or quoted
$value = $this->escape($value);
@@ -391,73 +425,81 @@ private function escapeAttributes($attrs)
return $script;
}
- private function getLayoutVertex(Vertex $vertex, $vid)
+ /**
+ * @return array
+ */
+ private function getLayoutVertex(Vertex $vertex, int|float|string $vid): array
{
$layout = $this->getAttributesPrefixed($vertex, 'graphviz.');
$balance = $vertex->getAttribute($this->attributeBalance);
- if ($balance !== NULL) {
+ if ($balance !== null) {
if ($balance > 0) {
$balance = '+' . $balance;
}
if (!isset($layout['label'])) {
- $layout['label'] = $vid;
+ $layout['label'] = (string) $vid;
}
- $layout['label'] .= ' (' . $balance . ')';
+ $layout['label'] .= " ($balance)";
}
return $layout;
}
- protected function getLayoutEdge(Edge $edge)
+ /**
+ * @return array
+ */
+ protected function getLayoutEdge(Edge $edge): array
{
$layout = $this->getAttributesPrefixed($edge, 'graphviz.');
// use flow/capacity/weight as edge label
- $label = NULL;
+ $label = null;
$flow = $edge->getAttribute($this->attributeFlow);
$capacity = $edge->getAttribute($this->attributeCapacity);
// flow is set
- if ($flow !== NULL) {
+ if ($flow !== null) {
// NULL capacity = infinite capacity
- $label = $flow . '/' . ($capacity === NULL ? '∞' : $capacity);
+ $label = $flow . '/' . ($capacity === null ? '∞' : $capacity);
// capacity set, but not flow (assume zero flow)
- } elseif ($capacity !== NULL) {
+ } elseif ($capacity !== null) {
$label = '0/' . $capacity;
}
$weight = $edge->getAttribute($this->attributeWeight);
// weight is set
- if ($weight !== NULL) {
- if ($label === NULL) {
+ if ($weight !== null) {
+ if ($label === null) {
$label = $weight;
} else {
$label .= '/' . $weight;
}
}
- if ($label !== NULL) {
+ if ($label !== null) {
if (isset($layout['label'])) {
$layout['label'] .= ' ' . $label;
} else {
$layout['label'] = $label;
}
}
+
return $layout;
}
/**
- * @param Graph|Vertex|Edge $entity
- * @param string $prefix
- * @return array
+ * @param \Graphp\Graph\Graph|\Graphp\Graph\Vertex|\Graphp\Graph\Edge $entity
+ * @param string $prefix
+ *
+ * @return array
*/
- private function getAttributesPrefixed(Entity $entity, $prefix)
+ private function getAttributesPrefixed(Entity $entity, string $prefix): array
{
$len = \strlen($prefix);
- $attributes = array();
+ $attributes = [];
foreach ($entity->getAttributes() as $name => $value) {
- if (\strpos($name, $prefix) === 0) {
+ if (str_starts_with($name, $prefix)) {
$attributes[substr($name, $len)] = $value;
}
}
diff --git a/src/Image.php b/src/Image.php
index 2d10ed2..7163477 100644
--- a/src/Image.php
+++ b/src/Image.php
@@ -1,14 +1,19 @@
graphviz = $graphviz;
}
- public function getOutput(Graph $graph)
+ public function getOutput(Graph $graph): string
{
return $this->graphviz->createImageData($graph);
}
@@ -26,13 +31,15 @@ public function getOutput(Graph $graph)
/**
* set the image output format to use
*
- * @param string $type png, svg
- * @return self $this (chainable)
+ * @param string $type
+ * png, svg
+ *
* @uses GraphViz::setFormat()
*/
- public function setFormat($type)
+ public function setFormat(string $type): static
{
$this->graphviz->setFormat($type);
+
return $this;
}
}
diff --git a/tests/GraphVizTest.php b/tests/GraphVizTest.php
deleted file mode 100644
index 57678de..0000000
--- a/tests/GraphVizTest.php
+++ /dev/null
@@ -1,424 +0,0 @@
-graphViz = new GraphViz();
- }
-
- public function testGraphEmpty()
- {
- $graph = new Graph();
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphWithName()
- {
- $graph = new Graph();
- $graph->setAttribute('graphviz.name', 'G');
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphWithNameWithSpaces()
- {
- $graph = new Graph();
- $graph->setAttribute('graphviz.name', 'My Graph Name');
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphIsolatedVertices()
- {
- $graph = new Graph();
- $graph->createVertex()->setAttribute('id', 'a');
- $graph->createVertex()->setAttribute('id', 'b');
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphIsolatedVerticesWillAssignNumericIdsWhenNotExplicitlyGiven()
- {
- $graph = new Graph();
- $graph->createVertex();
- $graph->createVertex();
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphIsolatedVerticesWithGroupsWillBeAddedToClusters()
- {
- $graph = new Graph();
- $graph->createVertex()->setAttribute('id', 'a')->setAttribute('group', 0);
- $graph->createVertex()->setAttribute('id', 'b')->setAttribute('group', 'foo bar')->setAttribute('graphviz.label', 'second');
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphDefaultAttributes()
- {
- $graph = new Graph();
- $graph->setAttribute('graphviz.graph.bgcolor', 'transparent');
- $graph->setAttribute('graphviz.node.color', 'blue');
- $graph->setAttribute('graphviz.edge.color', 'grey');
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testUnknownGraphAttributesWillBeDiscarded()
- {
- $graph = new Graph();
- $graph->setAttribute('graphviz.vertex.color', 'blue');
- $graph->setAttribute('graphviz.unknown.color', 'red');
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testEscaping()
- {
- $graph = new Graph();
- $graph->createVertex()->setAttribute('id', 'a');
- $graph->createVertex()->setAttribute('id', 'b¹²³ is; ok\\ay, "right"?');
- $graph->createVertex()->setAttribute('id', 3);
- $graph->createVertex()->setAttribute('id', 4)->setAttribute('graphviz.label', 'normal');
- $graph->createVertex()->setAttribute('id', 5)->setAttribute('graphviz.label_html', 'html-like');
- $graph->createVertex()->setAttribute('id', 6)->setAttribute('graphviz.label_html', 'hello
wörld');
- $graph->createVertex()->setAttribute('id', 7)->setAttribute('graphviz.label_record', 'first|{second1|second2}');
- $graph->createVertex()->setAttribute('id', 8)->setAttribute('graphviz.label_record', '"\N"');
-
- $expected = <<html-like>]
- 6 [label=wörld>]
- 7 [label="first|{second1|second2}"]
- 8 [label="\\"\\N\\""]
-}
-
-VIZ;
-
- $this->assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphWithSimpleEdgeUsesGraphWithSimpleEdgeDefinition()
- {
- // a -- b
- $graph = new Graph();
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', 'a'), $graph->createVertex()->setAttribute('id', 'b'));
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphWithLoopUsesGraphWithSimpleLoopDefinition()
- {
- // a -- b -\
- // | |
- // \--/
- $graph = new Graph();
- $a = $graph->createVertex()->setAttribute('id', 'a');
- $b = $graph->createVertex()->setAttribute('id', 'b');
- $graph->createEdgeUndirected($a, $b);
- $graph->createEdgeUndirected($b, $b);
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphDirectedUsesDigraph()
- {
- // a -> b
- $graph = new Graph();
- $graph->createEdgeDirected($graph->createVertex()->setAttribute('id', 'a'), $graph->createVertex()->setAttribute('id', 'b'));
-
- $expected = << "b"
-}
-
-VIZ;
-
- $this->assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphDirectedWithLoopUsesDigraphWithSimpleLoopDefinition()
- {
- // a -> b -\
- // ^ |
- // \--/
- $graph = new Graph();
- $a = $graph->createVertex()->setAttribute('id', 'a');
- $b = $graph->createVertex()->setAttribute('id', 'b');
- $graph->createEdgeDirected($a, $b);
- $graph->createEdgeDirected($b, $b);
-
- $expected = << "b"
- "b" -> "b"
-}
-
-VIZ;
-
- $this->assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphMixedUsesDigraphWithExplicitDirectionNoneForUndirectedEdges()
- {
- // a -> b -- c
- $graph = new Graph();
- $a = $graph->createVertex()->setAttribute('id', 'a');
- $b = $graph->createVertex()->setAttribute('id', 'b');
- $c = $graph->createVertex()->setAttribute('id', 'c');
- $graph->createEdgeDirected($a, $b);
- $graph->createEdgeUndirected($c, $b);
-
- $expected = << "b"
- "c" -> "b" [dir="none"]
-}
-
-VIZ;
-
- $this->assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphMixedWithDirectedLoopUsesDigraphWithoutDirectionForDirectedLoop()
- {
- // a -- b -\
- // ^ |
- // \--/
- $graph = new Graph();
- $a = $graph->createVertex()->setAttribute('id', 'a');
- $b = $graph->createVertex()->setAttribute('id', 'b');
- $graph->createEdgeUndirected($a, $b);
- $graph->createEdgeDirected($b, $b);
-
- $expected = << "b" [dir="none"]
- "b" -> "b"
-}
-
-VIZ;
-
- $this->assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testGraphUndirectedWithIsolatedVerticesFirst()
- {
- // a -- b -- c d
- $graph = new Graph();
- $a = $graph->createVertex()->setAttribute('id', 'a');
- $b = $graph->createVertex()->setAttribute('id', 'b');
- $c = $graph->createVertex()->setAttribute('id', 'c');
- $graph->createVertex()->setAttribute('id', 'd');
- $graph->createEdgeUndirected($a, $b);
- $graph->createEdgeUndirected($b, $c);
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testVertexLabels()
- {
- $graph = new Graph();
- $graph->createVertex()->setAttribute('id', 'a')->setAttribute('balance', 1);
- $graph->createVertex()->setAttribute('id', 'b')->setAttribute('balance', 0);
- $graph->createVertex()->setAttribute('id', 'c')->setAttribute('balance', -1);
- $graph->createVertex()->setAttribute('id', 'd')->setAttribute('graphviz.label', 'test');
- $graph->createVertex()->setAttribute('id', 'e')->setAttribute('balance', 2)->setAttribute('graphviz.label', 'unnamed');
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testEdgeLayoutAtributes()
- {
- $graph = new Graph();
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '1a'), $graph->createVertex()->setAttribute('id', '1b'));
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '2a'), $graph->createVertex()->setAttribute('id', '2b'))->setAttribute('graphviz.numeric', 20);
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '3a'), $graph->createVertex()->setAttribute('id', '3b'))->setAttribute('graphviz.textual', "forty");
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '4a'), $graph->createVertex()->setAttribute('id', '4b'))->setAttribute('graphviz.1', 1)->setAttribute('graphviz.2', 2);
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '5a'), $graph->createVertex()->setAttribute('id', '5b'))->setAttribute('graphviz.a', 'b')->setAttribute('graphviz.c', 'd');
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testEdgeLabels()
- {
- $graph = new Graph();
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '1a'), $graph->createVertex()->setAttribute('id', '1b'));
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '2a'), $graph->createVertex()->setAttribute('id', '2b'))->setAttribute('weight', 20);
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '3a'), $graph->createVertex()->setAttribute('id', '3b'))->setAttribute('capacity', 30);
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '4a'), $graph->createVertex()->setAttribute('id', '4b'))->setAttribute('flow', 40);
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '5a'), $graph->createVertex()->setAttribute('id', '5b'))->setAttribute('flow', 50)->setAttribute('capacity', 60);
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '6a'), $graph->createVertex()->setAttribute('id', '6b'))->setAttribute('flow', 60)->setAttribute('capacity', 70)->setAttribute('weight', 80);
- $graph->createEdgeUndirected($graph->createVertex()->setAttribute('id', '7a'), $graph->createVertex()->setAttribute('id', '7b'))->setAttribute('flow', 70)->setAttribute('graphviz.label', 'prefixed');
-
- $expected = <<assertEquals($expected, $this->graphViz->createScript($graph));
- }
-
- public function testCreateImageSrcWillExportPngDefaultFormat()
- {
- $graph = new Graph();
-
- $src = $this->graphViz->createImageSrc($graph);
-
- $this->assertStringStartsWith('data:image/png;base64,', $src);
- }
-
- public function testCreateImageSrcAsSvgWithUtf8DefaultCharset()
- {
- $graph = new Graph();
-
- $this->graphViz->setFormat('svg');
- $src = $this->graphViz->createImageSrc($graph);
-
- $this->assertStringStartsWith('data:image/svg+xml;charset=UTF-8;base64,', $src);
- }
-
- public function testCreateImageSrcAsSvgzWithExplicitIsoCharsetLatin1()
- {
- $graph = new Graph();
- $graph->setAttribute('graphviz.graph.charset', 'iso-8859-1');
-
- $this->graphViz->setFormat('svgz');
- $src = $this->graphViz->createImageSrc($graph);
-
- $this->assertStringStartsWith('data:image/svg+xml;charset=iso-8859-1;base64,', $src);
- }
-}
diff --git a/tests/src/Unit/DotTest.php b/tests/src/Unit/DotTest.php
new file mode 100644
index 0000000..a411325
--- /dev/null
+++ b/tests/src/Unit/DotTest.php
@@ -0,0 +1,40 @@
+
+ */
+ public static function casesGetOutput(): array
+ {
+ return [
+ 'empty' => [
+ 'expected' => <<< 'TEXT'
+ graph {
+ }
+
+ TEXT,
+ 'graph' => new Graph(),
+ ],
+ ];
+ }
+
+ #[DataProvider('casesGetOutput')]
+ public function testGetOutput(string $expected, Graph $graph): void
+ {
+ $dot = new Dot();
+ self::assertSame($expected, $dot->getOutput($graph));
+ }
+}
diff --git a/tests/src/Unit/GraphVizTest.php b/tests/src/Unit/GraphVizTest.php
new file mode 100644
index 0000000..dc9dbf8
--- /dev/null
+++ b/tests/src/Unit/GraphVizTest.php
@@ -0,0 +1,556 @@
+createGraphViz();
+ self::assertNotEquals('foo', $graphViz->getExecutable());
+ $graphViz->setExecutable('foo');
+ self::assertSame('foo', $graphViz->getExecutable());
+ }
+
+ public function testGraphEmpty(): void
+ {
+ $graph = new Graph();
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphWithName(): void
+ {
+ $graph = new Graph();
+ $graph->setAttribute('graphviz.name', 'G');
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphWithNameWithSpaces(): void
+ {
+ $graph = new Graph();
+ $graph->setAttribute('graphviz.name', 'My Graph Name');
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphIsolatedVertices(): void
+ {
+ $graph = new Graph();
+ $graph->createVertex()->setAttribute('id', 'a');
+ $graph->createVertex()->setAttribute('id', 'b');
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphIsolatedVerticesWillAssignNumericIdsWhenNotExplicitlyGiven(): void
+ {
+ $graph = new Graph();
+ $graph->createVertex();
+ $graph->createVertex();
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphIsolatedVerticesWithGroupsWillBeAddedToClusters(): void
+ {
+ $graph = new Graph();
+ $graph
+ ->createVertex()
+ ->setAttribute('id', 'a')
+ ->setAttribute('group', 0);
+ $graph
+ ->createVertex()
+ ->setAttribute('id', 'b')
+ ->setAttribute('group', 'foo bar')
+ ->setAttribute('graphviz.label', 'second');
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphDefaultAttributes(): void
+ {
+ $graph = new Graph();
+ $graph->setAttribute('graphviz.graph.bgcolor', 'transparent');
+ $graph->setAttribute('graphviz.node.color', 'blue');
+ $graph->setAttribute('graphviz.edge.color', 'grey');
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testUnknownGraphAttributesWillBeDiscarded(): void
+ {
+ $graph = new Graph();
+ $graph->setAttribute('graphviz.vertex.color', 'blue');
+ $graph->setAttribute('graphviz.unknown.color', 'red');
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testEscaping(): void
+ {
+ $graph = new Graph();
+ $graph->createVertex()->setAttribute('id', 'a');
+ $graph->createVertex()->setAttribute('id', 'b¹²³ is; ok\\ay, "right"?');
+ $graph->createVertex()->setAttribute('id', 3);
+ $graph
+ ->createVertex()
+ ->setAttribute('id', 4)
+ ->setAttribute('graphviz.label', 'normal');
+ $graph
+ ->createVertex()
+ ->setAttribute('id', 5)
+ ->setAttribute('graphviz.label_html', 'html-like');
+ $graph
+ ->createVertex()
+ ->setAttribute('id', 6)
+ ->setAttribute('graphviz.label_html', 'hello
wörld');
+ $graph
+ ->createVertex()
+ ->setAttribute('id', 7)
+ ->setAttribute('graphviz.label_record', 'first|{second1|second2}');
+ $graph
+ ->createVertex()
+ ->setAttribute('id', 8)
+ ->setAttribute('graphviz.label_record', '"\N"');
+
+ $expected = <<html-like>]
+ 6 [label=wörld>]
+ 7 [label="first|{second1|second2}"]
+ 8 [label="\\"\\N\\""]
+ }
+
+ VIZ;
+
+ $graphViz = $this->createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphWithSimpleEdgeUsesGraphWithSimpleEdgeDefinition(): void
+ {
+ // a -- b
+ $graph = new Graph();
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', 'a'),
+ $graph->createVertex()->setAttribute('id', 'b'),
+ );
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphWithLoopUsesGraphWithSimpleLoopDefinition(): void
+ {
+ // a -- b -\
+ // | |
+ // \--/
+ $graph = new Graph();
+ $a = $graph->createVertex()->setAttribute('id', 'a');
+ $b = $graph->createVertex()->setAttribute('id', 'b');
+ $graph->createEdgeUndirected($a, $b);
+ $graph->createEdgeUndirected($b, $b);
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphDirectedUsesDigraph(): void
+ {
+ // a -> b
+ $graph = new Graph();
+ $graph->createEdgeDirected(
+ $graph->createVertex()->setAttribute('id', 'a'),
+ $graph->createVertex()->setAttribute('id', 'b'),
+ );
+
+ $expected = << "b"
+ }
+
+ VIZ;
+
+ $graphViz = $this->createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphDirectedWithLoopUsesDigraphWithSimpleLoopDefinition(): void
+ {
+ // a -> b -\
+ // ^ |
+ // \--/
+ $graph = new Graph();
+ $a = $graph->createVertex()->setAttribute('id', 'a');
+ $b = $graph->createVertex()->setAttribute('id', 'b');
+ $graph->createEdgeDirected($a, $b);
+ $graph->createEdgeDirected($b, $b);
+
+ $expected = << "b"
+ "b" -> "b"
+ }
+
+ VIZ;
+
+ $graphViz = $this->createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphMixedUsesDigraphWithExplicitDirectionNoneForUndirectedEdges(): void
+ {
+ // a -> b -- c
+ $graph = new Graph();
+ $a = $graph->createVertex()->setAttribute('id', 'a');
+ $b = $graph->createVertex()->setAttribute('id', 'b');
+ $c = $graph->createVertex()->setAttribute('id', 'c');
+ $graph->createEdgeDirected($a, $b);
+ $graph->createEdgeUndirected($c, $b);
+
+ $expected = << "b"
+ "c" -> "b" [dir="none"]
+ }
+
+ VIZ;
+
+ $graphViz = $this->createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphMixedWithDirectedLoopUsesDigraphWithoutDirectionForDirectedLoop(): void
+ {
+ // a -- b -\
+ // ^ |
+ // \--/
+ $graph = new Graph();
+ $a = $graph->createVertex()->setAttribute('id', 'a');
+ $b = $graph->createVertex()->setAttribute('id', 'b');
+ $graph->createEdgeUndirected($a, $b);
+ $graph->createEdgeDirected($b, $b);
+
+ $expected = << "b" [dir="none"]
+ "b" -> "b"
+ }
+
+ VIZ;
+
+ $graphViz = $this->createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testGraphUndirectedWithIsolatedVerticesFirst(): void
+ {
+ // a -- b -- c d
+ $graph = new Graph();
+ $a = $graph->createVertex()->setAttribute('id', 'a');
+ $b = $graph->createVertex()->setAttribute('id', 'b');
+ $c = $graph->createVertex()->setAttribute('id', 'c');
+ $graph->createVertex()->setAttribute('id', 'd');
+ $graph->createEdgeUndirected($a, $b);
+ $graph->createEdgeUndirected($b, $c);
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testVertexLabels(): void
+ {
+ $graph = new Graph();
+ $graph->createVertex()->setAttribute('id', 'a')->setAttribute('balance', 1);
+ $graph->createVertex()->setAttribute('id', 'b')->setAttribute('balance', 0);
+ $graph->createVertex()->setAttribute('id', 'c')->setAttribute('balance', -1);
+ $graph->createVertex()->setAttribute('id', 'd')->setAttribute('graphviz.label', 'test');
+ $graph
+ ->createVertex()
+ ->setAttribute('id', 'e')
+ ->setAttribute('balance', 2)
+ ->setAttribute('graphviz.label', 'unnamed');
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testEdgeLayoutAttributes(): void
+ {
+ $graph = new Graph();
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '1a'),
+ $graph->createVertex()->setAttribute('id', '1b'),
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '2a'),
+ $graph->createVertex()->setAttribute('id', '2b'),
+ [
+ 'graphviz.numeric' => 20,
+ ],
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '3a'),
+ $graph->createVertex()->setAttribute('id', '3b'),
+ [
+ 'graphviz.textual' => 'forty',
+ ],
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '4a'),
+ $graph->createVertex()->setAttribute('id', '4b'),
+ [
+ 'graphviz.1' => 1,
+ 'graphviz.2' => 2,
+ ],
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '5a'),
+ $graph->createVertex()->setAttribute('id', '5b'),
+ [
+ 'graphviz.a' => 'b',
+ 'graphviz.c' => 'd',
+ ],
+ );
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testEdgeLabels(): void
+ {
+ $graph = new Graph();
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '1a'),
+ $graph->createVertex()->setAttribute('id', '1b'),
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '2a'),
+ $graph->createVertex()->setAttribute('id', '2b'),
+ [
+ 'weight' => 20,
+ ],
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '3a'),
+ $graph->createVertex()->setAttribute('id', '3b'),
+ [
+ 'capacity' => 30,
+ ],
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '4a'),
+ $graph->createVertex()->setAttribute('id', '4b'),
+ [
+ 'flow' => 40,
+ ],
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '5a'),
+ $graph->createVertex()->setAttribute('id', '5b'),
+ [
+ 'flow' => 50,
+ 'capacity' => 60,
+ ],
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '6a'),
+ $graph->createVertex()->setAttribute('id', '6b'),
+ [
+ 'flow' => 60,
+ 'capacity' => 70,
+ 'weight' => 80,
+ ],
+ );
+ $graph->createEdgeUndirected(
+ $graph->createVertex()->setAttribute('id', '7a'),
+ $graph->createVertex()->setAttribute('id', '7b'),
+ [
+ 'flow' => 70,
+ 'graphviz.label' => 'prefixed',
+ ],
+ );
+
+ $expected = <<createGraphViz();
+ self::assertSame($expected, $graphViz->createScript($graph));
+ }
+
+ public function testCreateImageSrcWillExportPngDefaultFormat(): void
+ {
+ $graph = new Graph();
+ $graphViz = $this->createGraphViz();
+ static::assertStringStartsWith('data:image/png;base64,', $graphViz->createImageSrc($graph));
+ }
+
+ public function testCreateImageSrcAsSvgWithUtf8DefaultCharset(): void
+ {
+ $graph = new Graph();
+ $graphViz = $this->createGraphViz();
+ $graphViz->setFormat('svg');
+ static::assertStringStartsWith(
+ 'data:image/svg+xml;charset=UTF-8;base64,',
+ $graphViz->createImageSrc($graph),
+ );
+ }
+
+ public function testCreateImageSrcAsSvgzWithExplicitIsoCharsetLatin1(): void
+ {
+ $graph = new Graph();
+ $graph->setAttribute('graphviz.graph.charset', 'iso-8859-1');
+ $graphViz = $this->createGraphViz();
+ $graphViz->setFormat('svgz');
+ static::assertStringStartsWith(
+ 'data:image/svg+xml;charset=iso-8859-1;base64,',
+ $graphViz->createImageSrc($graph),
+ );
+ }
+}
diff --git a/tests/src/Unit/ImageTest.php b/tests/src/Unit/ImageTest.php
new file mode 100644
index 0000000..339bbe1
--- /dev/null
+++ b/tests/src/Unit/ImageTest.php
@@ -0,0 +1,45 @@
+getOutput($graph),
+ 'By default it is PNG',
+ );
+
+ $graphViz = new GraphViz();
+ $graphViz->setFormat('svg');
+ $image = new Image($graphViz);
+ self::assertStringStartsWith(
+ 'getOutput($graph),
+ 'Inherited format is used.',
+ );
+
+ $image = new Image();
+ $image->setFormat('svg');
+ self::assertStringStartsWith(
+ 'getOutput($graph),
+ 'Format is overridden to SVG.',
+ );
+ }
+}