src/Engine/GraphBuilder.php
<?php
namespace Arokettu\ComposerViz\Engine;
use Arokettu\ComposerViz\Helpers\StringHelper;
use Composer\Composer;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\PackageInterface;
use Fhaculty\Graph\Edge\Directed as Edge;
use Fhaculty\Graph\Graph;
use Fhaculty\Graph\Vertex;
/**
* @internal
*/
final class GraphBuilder
{
const VERTEX_TYPE_DEFAULT = 'vertex_default';
const VERTEX_TYPE_ROOT = 'vertex_root';
const VERTEX_TYPE_DEV = 'vertex_dev';
const VERTEX_TYPE_PLATFORM = 'vertex_platform';
const VERTEX_TYPE_PROVIDED = 'vertex_provided';
private static $vertexColors = [
self::VERTEX_TYPE_DEFAULT => '#ffffff',
self::VERTEX_TYPE_ROOT => '#eeffee',
self::VERTEX_TYPE_DEV => '#eeeeee',
self::VERTEX_TYPE_PLATFORM => '#eeeeff',
self::VERTEX_TYPE_PROVIDED => '#ffeeee',
];
const EDGE_TYPE_DEFAULT = 'edge_default';
const EDGE_TYPE_DEV = 'edge_dev';
const EDGE_TYPE_PROVIDED = 'edge_provided';
private static $edgeColors = [
self::EDGE_TYPE_DEFAULT => '#000000',
self::EDGE_TYPE_DEV => '#777777',
self::EDGE_TYPE_PROVIDED => '#cc7777',
];
const NODE_ROOT = 'root_node';
const NODE_DEP = 'dep_node';
const NODE_DEV = 'dev_node';
private static $nodeBorderColors = [
self::NODE_ROOT => '#000000',
self::NODE_DEP => '#000000',
self::NODE_DEV => '#777777',
];
const PACKAGE_REGULAR = 'regular_package';
const PACKAGE_PHP = 'php_package';
const PACKAGE_EXTENSION = 'ext_package';
const PACKAGE_COMPOSER = 'composer_package';
/** @var Composer */
private $composer;
/** @var ArrayLoader */
private $arrayLoader;
/** @var Graph */
private $graph = null;
/** @var Vertex[] */
private $vertices = [];
/** @var Vertex[] */
private $phpVertices = [];
/** @var string[][] */
private $provides = [];
private $noDev;
private $noExt;
private $noPHP;
private $noVertexVersions;
private $noEdgeVersions;
public function __construct(Composer $composer, $noDev, $noExt, $noPHP, $noVertexVersions, $noEdgeVersions)
{
$this->noDev = $noDev;
$this->noExt = $noExt;
$this->noPHP = $noPHP;
$this->noVertexVersions = $noVertexVersions;
$this->noEdgeVersions = $noEdgeVersions;
$this->composer = $composer;
$this->arrayLoader = new ArrayLoader();
}
/**
* @return Graph
*/
public function build()
{
if ($this->graph) {
return $this->graph;
}
$this->graph = new Graph();
$this->graph->setAttribute('graphviz.graph.concentrate', 'true');
$dataComposerJson = $this->composer->getPackage();
$dataComposerLock = $this->composer->getLocker()->getLockData();
$this->processPackageData($dataComposerJson, self::NODE_ROOT, !$this->noDev);
$this->processLockFile($dataComposerLock, !$this->noDev);
foreach ($this->phpVertices as $vertex) {
$php = $this->getVertex('php', self::NODE_DEP);
$this->buildEdge($vertex, $php, '', self::EDGE_TYPE_PROVIDED);
}
foreach ($this->provides as list($package, $provided, $version)) {
if (!isset($this->vertices[$provided])) {
continue;
}
$packageVertex = $this->getVertex($package, self::NODE_DEP);
$providedVertex = $this->getVertex($provided, self::NODE_DEP);
$this->applyVertexStyle($providedVertex, self::VERTEX_TYPE_PROVIDED, self::NODE_DEP);
$this->buildEdge($providedVertex, $packageVertex, $version, self::EDGE_TYPE_PROVIDED);
}
return $this->graph;
}
/**
* @param PackageInterface $package
* @param bool $nodeType node type
* @param bool $includeDev include development dependencies
*/
private function processPackageData(PackageInterface $package, $nodeType, $includeDev)
{
$rootPackage = $package->getName();
$rootVertex = $this->getVertex($rootPackage, $nodeType);
if (!$this->noVertexVersions) {
$rootVertex->setAttribute('graphviz.label', "{$rootPackage}: {$package->getPrettyVersion()}");
}
foreach ($package->getRequires() as $link) {
$target = $link->getTarget();
if ($this->ignorePackage($target)) {
continue;
}
$constraint = $link->getPrettyConstraint();
$packageVertex = $this->getVertex(
$target,
$nodeType === self::NODE_DEV ? self::NODE_DEV : self::NODE_DEP
);
$this->buildEdge(
$rootVertex,
$packageVertex,
$constraint,
$nodeType === self::NODE_DEV ? self::EDGE_TYPE_DEV : self::EDGE_TYPE_DEFAULT
);
}
foreach ($package->getProvides() as $link) {
$this->provides[] = [$rootPackage, $link->getTarget(), $link->getPrettyConstraint()];
}
foreach ($package->getReplaces() as $link) {
$this->provides[] = [$rootPackage, $link->getTarget(), $link->getPrettyConstraint()];
}
if ($includeDev) {
foreach ($package->getDevRequires() as $link) {
$target = $link->getTarget();
if ($this->ignorePackage($target)) {
continue;
}
$constraint = $link->getPrettyConstraint();
$packageVertex = $this->getVertex($target, self::NODE_DEV);
$this->buildEdge($rootVertex, $packageVertex, $constraint, self::EDGE_TYPE_DEV);
}
}
}
private function getVertex($name, $nodeType)
{
if (!isset($this->vertices[$name])) {
$vertex = $this->graph->createVertex($name);
$packageType = $this->packageType($name);
if ($nodeType === self::NODE_ROOT) {
$vertexType = self::VERTEX_TYPE_ROOT;
} else {
switch ($packageType) {
case self::PACKAGE_EXTENSION:
case self::PACKAGE_COMPOSER:
case self::PACKAGE_PHP:
$vertexType = self::VERTEX_TYPE_PLATFORM;
break;
case self::PACKAGE_REGULAR:
$vertexType = $nodeType === self::NODE_DEV ? self::VERTEX_TYPE_DEV : self::VERTEX_TYPE_DEFAULT;
break;
default:
throw new \LogicException('Unknown package type');
}
}
$this->applyVertexStyle($vertex, $vertexType, $nodeType);
$this->vertices[$name] = $vertex;
// make php-64bit and php-ipv6 'provide' PHP
if ($packageType === self::PACKAGE_PHP && $name !== 'php') {
$this->phpVertices[$name] = $vertex;
}
}
return $this->vertices[$name];
}
private function buildEdge(Vertex $from, Vertex $to, $version, $edgeType)
{
$edge = $from->createEdgeTo($to);
if (!$this->noEdgeVersions) {
$edge->setAttribute('graphviz.label', $version);
}
$this->applyEdgeStyle($edge, $edgeType);
}
private function processLockFile($dataComposerLock, $dev)
{
$this->processPackageList($dataComposerLock['packages'], self::NODE_DEP);
if ($dev) {
$this->processPackageList($dataComposerLock['packages-dev'], self::NODE_DEV);
}
}
private function processPackageList(array $packages, $nodeType)
{
foreach ($packages as $package) {
$this->processPackageData($this->arrayLoader->load($package), $nodeType, false);
}
}
private function ignorePackage($name)
{
$type = $this->packageType($name);
// filter extensions (begins with ext-, no namespace slash)
if ($this->noExt && $type === self::PACKAGE_EXTENSION) {
return true;
}
// filter php platform (begins with php, no namespace slash)
if ($this->noPHP && $type === self::PACKAGE_PHP) {
return true;
}
return false;
}
private function packageType($name)
{
if (StringHelper::strContains($name, '/') || $name === '__root__') {
return self::PACKAGE_REGULAR;
}
if (StringHelper::strStartsWith($name, 'ext-') || StringHelper::strStartsWith($name, 'lib-')) {
return self::PACKAGE_EXTENSION;
}
if (StringHelper::strStartsWith($name, 'php')) {
return self::PACKAGE_PHP;
}
if (StringHelper::strStartsWith($name, 'composer-')) {
return self::PACKAGE_COMPOSER;
}
throw new \RuntimeException("Unable to determine package type of {$name}");
}
private function applyVertexStyle(Vertex $vertex, $vertexType, $nodeType)
{
$vertex->setAttribute('graphviz.shape', 'box');
$vertex->setAttribute('graphviz.style', 'rounded, filled');
$vertex->setAttribute('graphviz.fillcolor', self::$vertexColors[$vertexType]);
$vertex->setAttribute('graphviz.color', self::$nodeBorderColors[$nodeType]);
$vertex->setAttribute('graphviz.fontcolor', self::$nodeBorderColors[$nodeType]);
}
private function applyEdgeStyle(Edge $edge, $edgeType)
{
$edge->setAttribute('graphviz.color', self::$edgeColors[$edgeType]);
$edge->setAttribute('graphviz.fontcolor', self::$edgeColors[$edgeType]);
}
}