├── Dockerfile ├── LICENSE ├── README.md ├── composer-graph ├── composer-graph.php ├── composer.json └── src ├── Collection.php ├── Commands ├── Build.php └── Exception.php ├── ComposerGraph.php ├── Exception.php ├── Helper.php └── Package.php /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # JBZoo Toolbox - Composer-Graph. 3 | # 4 | # This file is part of the JBZoo Toolbox project. 5 | # For the full copyright and license information, please view the LICENSE 6 | # file that was distributed with this source code. 7 | # 8 | # @license MIT 9 | # @copyright Copyright (C) JBZoo.com, All rights reserved. 10 | # @see https://github.com/JBZoo/Composer-Graph 11 | # 12 | 13 | FROM php:8.1-cli-alpine 14 | RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" 15 | 16 | ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ 17 | RUN chmod +x /usr/local/bin/install-php-extensions \ 18 | && sync \ 19 | && install-php-extensions \ 20 | opcache \ 21 | gd \ 22 | zip \ 23 | @composer 24 | 25 | ENV COMPOSER_ALLOW_SUPERUSER=1 26 | COPY . /app 27 | RUN cd /app \ 28 | && composer install --no-dev --optimize-autoloader --no-progress \ 29 | && composer clear-cache 30 | 31 | # Experimental. Forced colored output 32 | ENV TERM_PROGRAM=Hyper 33 | 34 | ENTRYPOINT ["php", "/app/composer-graph"] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 JBZoo Toolbox for developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JBZoo / Composer-Graph 2 | 3 | [![CI](https://github.com/JBZoo/Composer-Graph/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/JBZoo/Composer-Graph/actions/workflows/main.yml?query=branch%3Amaster) [![Coverage Status](https://coveralls.io/repos/github/JBZoo/Composer-Graph/badge.svg?branch=master)](https://coveralls.io/github/JBZoo/Composer-Graph?branch=master) [![Psalm Coverage](https://shepherd.dev/github/JBZoo/Composer-Graph/coverage.svg)](https://shepherd.dev/github/JBZoo/Composer-Graph) [![Psalm Level](https://shepherd.dev/github/JBZoo/Composer-Graph/level.svg)](https://shepherd.dev/github/JBZoo/Composer-Graph) [![CodeFactor](https://www.codefactor.io/repository/github/jbzoo/composer-graph/badge)](https://www.codefactor.io/repository/github/jbzoo/composer-graph/issues) 4 | [![Stable Version](https://poser.pugx.org/jbzoo/composer-graph/version)](https://packagist.org/packages/jbzoo/composer-graph/) [![Total Downloads](https://poser.pugx.org/jbzoo/composer-graph/downloads)](https://packagist.org/packages/jbzoo/composer-graph/stats) [![Dependents](https://poser.pugx.org/jbzoo/composer-graph/dependents)](https://packagist.org/packages/jbzoo/composer-graph/dependents?order_by=downloads) [![GitHub License](https://img.shields.io/github/license/jbzoo/composer-graph)](https://github.com/JBZoo/Composer-Graph/blob/master/LICENSE) 5 | 6 | 7 | 8 | * [Installation](#installation) 9 | * [Usage](#usage) 10 | * [Examples](#examples) 11 | * [Default output (no args) - minimal view](#default-output-no-args---minimal-view) 12 | * [Default output with PHP extensions (modules)](#default-output-with-php-extensions-modules) 13 | * [Default output with versions of packages and relations](#default-output-with-versions-of-packages-and-relations) 14 | * [Show suggested packages which are not installed](#show-suggested-packages-which-are-not-installed) 15 | * [Show dev dependencies](#show-dev-dependencies) 16 | * [Full Report](#full-report) 17 | * [Unit tests and check code style](#unit-tests-and-check-code-style) 18 | * [License](#license) 19 | * [See Also](#see-also) 20 | 21 | 22 | ## Installation 23 | 24 | ```shell 25 | composer require jbzoo/composer-graph # For a specific project 26 | composer global require jbzoo/composer-graph # As global tool 27 | 28 | # OR use phar file. 29 | wget https://github.com/JBZoo/Composer-Graph/releases/latest/download/composer-graph.phar 30 | ``` 31 | 32 | 33 | ## Usage 34 | 35 | ``` 36 | $ php ./vendor/bin/composer-graph --help 37 | 38 | Usage: 39 | build [options] 40 | 41 | Options: 42 | -r, --root=ROOT The path has to contain "composer.json" and "composer.lock" files [default: "./"] 43 | -o, --output=OUTPUT Path to html output. [default: "./build/composer-graph.html"] 44 | -f, --format=FORMAT Output format. Available options: html,mermaid [default: "html"] 45 | -D, --direction=DIRECTION Direction of graph. Available options: LR,TB,BT,RL [default: "LR"] 46 | -p, --show-php Show PHP-node 47 | -e, --show-ext Show all ext-* nodes (PHP modules) 48 | -d, --show-dev Show all dev dependencies 49 | -s, --show-suggests Show not installed suggests packages 50 | -l, --show-link-versions Show version requirements in links 51 | -P, --show-package-versions Show version of packages 52 | -O, --abc-order Strict ABC ordering nodes in graph. It's fine tuning, sometimes it useful. 53 | --no-progress Disable progress bar animation for logs. It will be used only for text output format. 54 | --mute-errors Mute any sort of errors. So exit code will be always "0" (if it's possible). 55 | It has major priority then --non-zero-on-error. It's on your own risk! 56 | --stdout-only For any errors messages application will use StdOut instead of StdErr. It's on your own risk! 57 | --non-zero-on-error None-zero exit code on any StdErr message. 58 | --timestamp Show timestamp at the beginning of each message.It will be used only for text output format. 59 | --profile Display timing and memory usage information. 60 | --output-mode=OUTPUT-MODE Output format. Available options: 61 | text - Default text output format, userfriendly and easy to read. 62 | cron - Shortcut for crontab. It's basically focused on human-readable logs output. 63 | It's combination of --timestamp --profile --stdout-only --no-progress -vv. 64 | logstash - Logstash output format, for integration with ELK stack. 65 | [default: "text"] 66 | --cron Alias for --output-mode=cron. Deprecated! 67 | -h, --help Display help for the given command. When no command is given display help for the build command 68 | -q, --quiet Do not output any message 69 | -V, --version Display this application version 70 | --ansi|--no-ansi Force (or disable --no-ansi) ANSI output 71 | -n, --no-interaction Do not ask any interactive question 72 | -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 73 | 74 | ``` 75 | 76 | 77 | ## Examples 78 | 79 | All examples are screenshots based on the package [JBZoo/Toolbox](https://github.com/JBZoo/Toolbox). 80 | 81 | 82 | ### Default output (no args) - minimal view 83 | ```shell 84 | php ./vendor/bin/composer-graph 85 | ``` 86 | 87 | ![Example](https://raw.githubusercontent.com/JBZoo/Composer-Graph/master/resources/jbzoo-minimal.png) 88 | 89 | 90 | 91 | ### Default output with PHP extensions (modules) 92 | ```shell 93 | php ./vendor/bin/composer-graph --show-ext 94 | ``` 95 | 96 | ![Example](https://raw.githubusercontent.com/JBZoo/Composer-Graph/master/resources/jbzoo-extensions.png) 97 | 98 | 99 | 100 | ### Default output with versions of packages and relations 101 | ```shell 102 | php ./vendor/bin/composer-graph --show-link-versions --show-package-versions 103 | ``` 104 | 105 | ![Example](https://raw.githubusercontent.com/JBZoo/Composer-Graph/master/resources/jbzoo-versions.png) 106 | 107 | 108 | 109 | ### Show suggested packages which are not installed 110 | ```shell 111 | php ./vendor/bin/composer-graph --show-suggests 112 | ``` 113 | 114 | ![Example](https://raw.githubusercontent.com/JBZoo/Composer-Graph/master/resources/jbzoo-suggests.png) 115 | 116 | 117 | 118 | ### Show dev dependencies 119 | ```shell 120 | php ./vendor/bin/composer-graph --show-dev 121 | ``` 122 | 123 | ![Example](https://raw.githubusercontent.com/JBZoo/Composer-Graph/master/resources/jbzoo-dev.png) 124 | 125 | 126 | ### Full Report 127 | 128 | All options are enabled but `--show-php` (too many packages). 129 | 130 | ```shell 131 | php ./vendor/bin/composer-graph \ 132 | --show-ext \ 133 | --show-dev \ 134 | --show-suggests \ 135 | --show-link-versions \ 136 | --show-package-versions 137 | ``` 138 | 139 | ![Example](https://raw.githubusercontent.com/JBZoo/Composer-Graph/master/resources/jbzoo-full-without-php.png) 140 | 141 | 142 | 143 | 144 | ## Unit tests and check code style 145 | ```shell 146 | make update 147 | make test-all 148 | ``` 149 | 150 | 151 | ## License 152 | MIT 153 | 154 | 155 | ## See Also 156 | 157 | - [CI-Report-Converter](https://github.com/JBZoo/CI-Report-Converter) - Converting different error reports for deep compatibility with popular CI systems. 158 | - [Composer-Diff](https://github.com/JBZoo/Composer-Diff) - See what packages have changed after `composer update`. 159 | - [Mermaid-PHP](https://github.com/JBZoo/Mermaid-PHP) - Generate diagrams and flowcharts with the help of the mermaid script language. 160 | - [Utils](https://github.com/JBZoo/Utils) - Collection of useful PHP functions, mini-classes, and snippets for every day. 161 | - [Image](https://github.com/JBZoo/Image) - Package provides object-oriented way to manipulate with images as simple as possible. 162 | - [Data](https://github.com/JBZoo/Data) - Extended implementation of ArrayObject. Use files as config/array. 163 | - [Retry](https://github.com/JBZoo/Retry) - Tiny PHP library providing retry/backoff functionality with multiple backoff strategies and jitter support. 164 | - [SimpleTypes](https://github.com/JBZoo/SimpleTypes) - Converting any values and measures - money, weight, exchange rates, length, ... 165 | -------------------------------------------------------------------------------- /composer-graph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | registerCommandsByPath(__DIR__ . '/src/Commands', __NAMESPACE__); 40 | $application->setDefaultCommand('build'); 41 | $application->run(); 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "jbzoo/composer-graph", 3 | "type" : "library", 4 | "description" : "Render composer.json + composer.lock dependencies graph", 5 | "license" : "MIT", 6 | "keywords" : [ 7 | "composer", "diagram", "jbzoo", "dependencies", "composer-dependency", "composer-packages", "mermaidjs", 8 | "composer-graph", "graph" 9 | ], 10 | 11 | "authors" : [ 12 | { 13 | "name" : "Denis Smetannikov", 14 | "email" : "admin@jbzoo.com", 15 | "role" : "lead" 16 | } 17 | ], 18 | 19 | "minimum-stability" : "dev", 20 | "prefer-stable" : true, 21 | 22 | "require" : { 23 | "php" : "^8.1", 24 | 25 | "jbzoo/data" : "^7.1", 26 | "jbzoo/mermaid-php" : "^7.2", 27 | "jbzoo/utils" : "^7.1", 28 | "jbzoo/cli" : "^7.1.7", 29 | 30 | "symfony/console" : ">=6.4" 31 | }, 32 | 33 | "require-dev" : { 34 | "jbzoo/toolbox-dev" : "^7.1", 35 | "symfony/process" : ">=6.4" 36 | }, 37 | 38 | "autoload" : { 39 | "psr-4" : {"JBZoo\\ComposerGraph\\" : "src"} 40 | }, 41 | 42 | "autoload-dev" : { 43 | "psr-4" : {"JBZoo\\PHPUnit\\" : "tests"} 44 | }, 45 | 46 | "bin" : ["composer-graph"], 47 | 48 | "config" : { 49 | "optimize-autoloader" : true, 50 | "allow-plugins" : {"composer/package-versions-deprecated" : true}, 51 | "platform-check" : true 52 | }, 53 | 54 | "extra" : { 55 | "branch-alias" : { 56 | "dev-master" : "7.x-dev" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | composerFile = $composerFile; 37 | $this->lockFile = $lockFile; 38 | $this->vendorDir = $vendorDir; 39 | 40 | $this->buildCollection(); 41 | } 42 | 43 | /** 44 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 45 | * @SuppressWarnings(PHPMD.NPathComplexity) 46 | */ 47 | public function buildCollection(): void 48 | { 49 | /** @phpstan-ignore-next-line */ 50 | $istTest = \defined('\IS_PHPUNIT_TEST') && IS_PHPUNIT_TEST; 51 | 52 | $this->add('php', [ 53 | 'version' => $istTest ? null : \PHP_VERSION, 54 | 'tags' => [Package::PHP, Package::HAS_META], 55 | ]); 56 | 57 | $this->add((string)$this->composerFile->get('name'), [ 58 | 'version' => $istTest ? null : Helper::getGitVersion(), 59 | 'require' => $this->composerFile->get('require'), 60 | 'require-dev' => $this->composerFile->get('require-dev'), 61 | 'suggest' => $this->composerFile->get('suggest'), 62 | 'tags' => [Package::MAIN, Package::HAS_META], 63 | ]); 64 | 65 | $mainRequire = \array_keys((array)$this->composerFile->get('require')); 66 | 67 | foreach ($mainRequire as $package) { 68 | $this->add((string)$package, ['tags' => [Package::DIRECT]]); 69 | } 70 | 71 | $mainRequireDev = \array_keys((array)$this->composerFile->get('require-dev')); 72 | 73 | foreach ($mainRequireDev as $packageDev) { 74 | $this->add((string)$packageDev, ['tags' => [Package::DIRECT]]); 75 | } 76 | 77 | $mainSuggest = \array_keys((array)$this->composerFile->get('suggest')); 78 | 79 | foreach ($mainSuggest as $suggest) { 80 | $this->add((string)$suggest, ['tags' => [Package::DIRECT, Package::SUGGEST]]); 81 | } 82 | 83 | // Lock file 84 | $scopes = [ 85 | Package::REQUIRED => (array)$this->lockFile->get('packages'), 86 | Package::REQUIRED_DEV => (array)$this->lockFile->get('packages-dev'), 87 | ]; 88 | 89 | foreach ($scopes as $scopeType => $scope) { 90 | foreach ($scope as $package) { 91 | $package = json($package); 92 | 93 | $require = (array)$package->get('require'); 94 | $suggest = (array)$package->get('suggest'); 95 | 96 | $version = $package->getString('version'); 97 | $version = $package->findString("extra.branch-alias.{$version}", $version); 98 | 99 | $this->add((string)$package->get('name'), [ 100 | 'version' => $version, 101 | 'require' => $require, 102 | 'suggest' => $suggest, 103 | 'tags' => [$scopeType, Package::HAS_META], 104 | ]); 105 | 106 | foreach (\array_keys($require) as $innerRequired) { 107 | $this->add((string)$innerRequired, ['tags' => [$scopeType]]); 108 | } 109 | 110 | foreach (\array_keys($suggest) as $innerSuggested) { 111 | $this->add((string)$innerSuggested, ['tags' => [$scopeType, Package::SUGGEST]]); 112 | } 113 | } 114 | } 115 | } 116 | 117 | public function getMain(): Package 118 | { 119 | foreach ($this->collection as $package) { 120 | if ($package->isMain()) { 121 | return $package; 122 | } 123 | } 124 | 125 | throw new Exception('Main package not found'); 126 | } 127 | 128 | public function getByName(string $packageName): Package 129 | { 130 | $packageAlias = Package::alias($packageName); 131 | if (\array_key_exists($packageAlias, $this->collection)) { 132 | return $this->collection[$packageAlias]; 133 | } 134 | 135 | throw new Exception("Package \"{$packageName} ({$packageAlias})\" not found in collection"); 136 | } 137 | 138 | private function add(string $packageName, array $packageMeta): void 139 | { 140 | $current = json($packageMeta); 141 | $packageAlias = Package::alias($packageName); 142 | 143 | /** @var Package $package */ 144 | $package = $this->collection[$packageAlias] ?? new Package($packageName, $this->vendorDir); 145 | 146 | $package 147 | ->setVersion((string)$current->get('version')) 148 | ->addRequire((array)$current->get('require')) 149 | ->addRequireDev((array)$current->get('require-dev')) 150 | ->addSuggest((array)$current->get('suggest')) 151 | ->addTags((array)$current->get('tags')); 152 | 153 | $this->collection[$packageAlias] = $package; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Commands/Build.php: -------------------------------------------------------------------------------- 1 | setName('build') 49 | ->addOption( 50 | 'root', 51 | 'r', 52 | $required, 53 | 'The path has to contain ' . 54 | '"composer.json" and "composer.lock" files', 55 | './', 56 | ) 57 | ->addOption('output', 'o', $required, 'Path to html output.', './build/composer-graph.html') 58 | ->addOption( 59 | 'format', 60 | 'f', 61 | $required, 62 | 'Output format. Available options: ' . \implode(',', [ 63 | ComposerGraph::FORMAT_HTML, 64 | ComposerGraph::FORMAT_MERMAID, 65 | ]) . '', 66 | ComposerGraph::FORMAT_HTML, 67 | ) 68 | ->addOption( 69 | 'direction', 70 | 'D', 71 | $required, 72 | 'Direction of graph. Available options: ' . \implode(',', [ 73 | Graph::LEFT_RIGHT, 74 | Graph::TOP_BOTTOM, 75 | Graph::BOTTOM_TOP, 76 | Graph::RIGHT_LEFT, 77 | ]) . '', 78 | Graph::LEFT_RIGHT, 79 | ) 80 | ->addOption('show-php', 'p', $none, 'Show PHP-node') 81 | ->addOption('show-ext', 'e', $none, 'Show all ext-* nodes (PHP modules)') 82 | ->addOption('show-dev', 'd', $none, 'Show all dev dependencies') 83 | ->addOption('show-suggests', 's', $none, 'Show not installed suggests packages') 84 | ->addOption('show-link-versions', 'l', $none, 'Show version requirements in links') 85 | ->addOption('show-package-versions', 'P', $none, 'Show version of packages') 86 | ->addOption( 87 | 'abc-order', 88 | 'O', 89 | $none, 90 | 'Strict ABC ordering nodes in graph. ' . 91 | "It's fine tuning, sometimes it useful.", 92 | ); 93 | 94 | parent::configure(); 95 | } 96 | 97 | /** 98 | * {@inheritDoc} 99 | */ 100 | protected function executeAction(): int 101 | { 102 | $format = $this->getOptString('format'); 103 | 104 | [$composerJson, $composerLock] = $this->getJsonData(); 105 | $vendorDir = $this->findVendorDir($composerJson); 106 | 107 | $composerGraph = new ComposerGraph( 108 | new Collection($composerJson, $composerLock, $vendorDir), 109 | [ 110 | 'direction' => $this->getOptString('direction'), 111 | 'php' => $this->getOptBool('show-php'), 112 | 'ext' => $this->getOptBool('show-ext'), 113 | 'dev' => $this->getOptBool('show-dev'), 114 | 'suggest' => $this->getOptBool('show-suggests'), 115 | 'link-version' => $this->getOptBool('show-link-versions'), 116 | 'lib-version' => $this->getOptBool('show-package-versions'), 117 | 'format' => $format, 118 | 'output-path' => $this->getOptString('output'), 119 | 'abc-order' => $this->getOptBool('abc-order'), 120 | ], 121 | ); 122 | 123 | $result = $composerGraph->build(); 124 | if ($format === ComposerGraph::FORMAT_HTML) { 125 | $this->_("Report is ready: {$result}"); 126 | } else { 127 | $this->_($result); 128 | } 129 | 130 | return Codes::OK; 131 | } 132 | 133 | private function getRootPath(): string 134 | { 135 | $origRootPath = $this->getOptString('root'); 136 | $realRootPath = \realpath($origRootPath); 137 | 138 | // Validate root path 139 | if ($realRootPath === false || !\is_dir($realRootPath)) { 140 | throw new Exception("Root path is not directory or not found: {$origRootPath}"); 141 | } 142 | 143 | return $realRootPath; 144 | } 145 | 146 | /** 147 | * @return JSON[] 148 | */ 149 | private function getJsonData(): array 150 | { 151 | $realRootPath = $this->getRootPath(); 152 | 153 | // Validate "composer.json" path and file 154 | $composerJsonPath = "{$realRootPath}/composer.json"; 155 | if (!\file_exists($composerJsonPath)) { 156 | throw new Exception("The file \"{$composerJsonPath}\" not found"); 157 | } 158 | 159 | $composerJson = json($composerJsonPath); 160 | if (\count($composerJson) <= 1) { 161 | throw new Exception("The file \"{$composerJsonPath}\" is empty"); 162 | } 163 | 164 | $this->_("Composer JSON file found: {$composerJsonPath}", OutLvl::DEBUG); 165 | 166 | // Validate "composer.lock" path and file 167 | $composerLockPath = "{$realRootPath}/composer.lock"; 168 | if (!\file_exists($composerLockPath)) { 169 | throw new Exception("The file \"{$composerLockPath}\" not found"); 170 | } 171 | 172 | $composerLock = json($composerLockPath); 173 | if (\count($composerLock) <= 1) { 174 | throw new Exception("The file \"{$composerLockPath}\" is empty"); 175 | } 176 | 177 | $this->_("Composer LOCK file found: {$composerLockPath}", OutLvl::DEBUG); 178 | 179 | return [$composerJson, $composerLock]; 180 | } 181 | 182 | private function findVendorDir(JSON $composerJson): ?string 183 | { 184 | $realRootPath = $this->getRootPath(); 185 | 186 | $vendorDir = $composerJson->find('config.vendor-dir') ?? 'vendor'; 187 | 188 | $realVendorDir = \realpath("{$realRootPath}/{$vendorDir}"); 189 | if ( 190 | $realVendorDir !== false 191 | && \is_dir($realVendorDir) 192 | ) { 193 | return $realVendorDir; 194 | } 195 | 196 | return null; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Commands/Exception.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 57 | 58 | $this->params = data(\array_merge([ 59 | // view options 60 | 'php' => false, 61 | 'ext' => false, 62 | 'dev' => false, 63 | 'suggest' => false, 64 | 'link-version' => false, 65 | 'lib-version' => false, 66 | // output options 67 | 'output-path' => null, 68 | 'direction' => Graph::LEFT_RIGHT, 69 | 'format' => self::FORMAT_HTML, 70 | 'vendor-dir' => null, 71 | 'abc-order' => false, 72 | ], $params)); 73 | 74 | $direction = $this->params->getString('direction'); 75 | $order = $this->params->getBool('abc-order'); 76 | 77 | $this->graphWrapper = new Graph(['direction' => $direction, 'abc_order' => $order]); 78 | $this->graphWrapper->addSubGraph($this->graphMain = new Graph(['title' => 'Your Package'])); 79 | 80 | $this->graphRequire = new Graph(['direction' => $direction, 'title' => 'Required', 'abc_order' => $order]); 81 | $this->graphDev = new Graph(['direction' => $direction, 'title' => 'Required Dev', 'abc_order' => $order]); 82 | $this->graphPlatform = new Graph(['direction' => $direction, 'title' => 'PHP Platform', 'abc_order' => $order]); 83 | } 84 | 85 | public function build(): string 86 | { 87 | /** @phpstan-ignore-next-line */ 88 | $isSafeMode = \defined('\IS_PHPUNIT_TEST') && !IS_PHPUNIT_TEST; 89 | Node::safeMode($isSafeMode); 90 | 91 | $main = $this->collection->getMain(); 92 | $this->renderNodeTree($main); 93 | 94 | return $this->render(); 95 | } 96 | 97 | /** 98 | * @SuppressWarnings(PHPMD.NPathComplexity) 99 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 100 | */ 101 | public function renderNodeTree(Package $source): bool 102 | { 103 | if (\in_array($source->getId(), $this->renderedNodes, true)) { 104 | return false; 105 | } 106 | $this->renderedNodes[] = $source->getId(); 107 | 108 | $showPhp = $this->params->getBool('php'); 109 | $showExt = $this->params->getBool('ext'); 110 | $showDev = $this->params->getBool('dev'); 111 | $showSuggest = $this->params->getBool('suggest'); 112 | 113 | if (!$showSuggest && !$source->isTag(Package::HAS_META)) { 114 | return false; 115 | } 116 | 117 | foreach ($source->getRequired() as $target => $version) { 118 | $target = (string)$target; 119 | $target = $this->collection->getByName($target); 120 | if ( 121 | (!$showExt && $target->isPhpExt()) 122 | || (!$showPhp && $target->isPhp()) 123 | ) { 124 | continue; 125 | } 126 | 127 | $this->renderNodeTree($target); 128 | $this->addLink($source, $target, $version); 129 | } 130 | 131 | if ($showDev) { 132 | foreach ($source->getRequiredDev() as $target => $version) { 133 | $target = (string)$target; 134 | $target = $this->collection->getByName($target); 135 | if ( 136 | (!$showExt && $target->isPhpExt()) 137 | || (!$showPhp && $target->isPhp()) 138 | ) { 139 | continue; 140 | } 141 | 142 | $this->renderNodeTree($target); 143 | $this->addLink($source, $target, $version); 144 | } 145 | } 146 | 147 | if ($showSuggest) { 148 | foreach (\array_keys($source->getSuggested()) as $target) { 149 | $target = $this->collection->getByName((string)$target); 150 | if ( 151 | (!$showExt && $target->isPhpExt()) 152 | || ( 153 | !$showDev 154 | && !$target->isTag(Package::REQUIRED) 155 | && $target->isTag(Package::REQUIRED_DEV) 156 | ) 157 | ) { 158 | continue; 159 | } 160 | 161 | $this->renderNodeTree($target); 162 | $this->addLink($source, $target, 'suggest'); 163 | } 164 | } 165 | 166 | return true; 167 | } 168 | 169 | protected function render(): string 170 | { 171 | if (\count($this->graphRequire->getNodes()) > 0) { 172 | $this->graphWrapper->addSubGraph($this->graphRequire); 173 | } 174 | 175 | if (\count($this->graphDev->getNodes()) > 0) { 176 | $this->graphWrapper->addSubGraph($this->graphDev); 177 | } 178 | 179 | if (\count($this->graphPlatform->getNodes()) > 0) { 180 | $this->graphWrapper->addSubGraph($this->graphPlatform); 181 | } 182 | 183 | $format = \strtolower(\trim($this->params->getString('format'))); 184 | if ($format === self::FORMAT_HTML) { 185 | $htmlPath = (string)$this->params->get('output-path'); 186 | 187 | $headerKeys = \array_filter( 188 | \array_keys( 189 | $this->params->getArrayCopy(), 190 | static fn (string $key): bool => \in_array( 191 | $key, 192 | ['php', 'ext', 'dev', 'suggest', 'link-version', 'lib-version'], 193 | true, 194 | ), 195 | true, 196 | ), 197 | ); 198 | 199 | /** 200 | * @psalm-suppress InvalidArgument 201 | * @phpstan-ignore-next-line 202 | */ 203 | $headerKeys = \array_reduce($headerKeys, function (array $acc, string $key): array { 204 | if (bool($this->params->get($key))) { 205 | $acc[] = $key; 206 | } 207 | 208 | return $acc; 209 | }, []); 210 | 211 | $titlePostfix = ''; 212 | if (\count($headerKeys) > 0) { 213 | $flags = \implode(' / ', $headerKeys); 214 | $titlePostfix = "\n
Flags: {$flags}"; 215 | } 216 | 217 | $main = $this->collection->getMain(); 218 | $htmlDir = \dirname($htmlPath); 219 | if (!\is_dir($htmlDir) && !\mkdir($htmlDir, 0755, true) && !\is_dir($htmlDir)) { 220 | throw new \RuntimeException("Directory \"{$htmlDir}\" was not created"); 221 | } 222 | 223 | \file_put_contents($htmlPath, $this->graphWrapper->renderHtml([ 224 | 'version' => '8.6.0', 225 | 'title' => $main->getName() . ' - Graph of Dependencies' . $titlePostfix, 226 | ])); 227 | 228 | return $htmlPath; 229 | } 230 | 231 | if ($format === self::FORMAT_MERMAID) { 232 | return $this->graphWrapper->render(); 233 | } 234 | 235 | throw new Exception("Invalid format: \"{$format}\""); 236 | } 237 | 238 | protected function createNode(Package $package): Node 239 | { 240 | $graph = $this->getGraph($package); 241 | 242 | $nodeId = $package->getId(); 243 | $showVersion = (bool)$this->params->getString('lib-version'); 244 | 245 | $currentNode = $graph->getNode($nodeId); 246 | if ($currentNode !== null) { 247 | return $currentNode; 248 | } 249 | 250 | $vendorDir = $this->params->getString('vendor-dir'); 251 | if ($vendorDir !== '') { 252 | $isInstalled = $package->isTag(Package::INSTALLED); 253 | } else { 254 | $isInstalled = $package->isTag(Package::HAS_META); 255 | } 256 | 257 | if ($isInstalled) { 258 | $node = new Node($nodeId, $package->getName($showVersion)); 259 | } else { 260 | $node = new Node($nodeId, $package->getName($showVersion), Node::STADIUM); 261 | } 262 | 263 | $graph->addNode($node); 264 | 265 | return $node; 266 | } 267 | 268 | private function addLink(Package $source, Package $target, string $version): void 269 | { 270 | $sourceName = $source->getId(); 271 | $targetName = $target->getId(); 272 | $version = $version === '*' ? '' : $version; 273 | 274 | $pattern = "{$sourceName}=={$targetName}"; 275 | 276 | if (!\array_key_exists($pattern, $this->createdLinks)) { 277 | $sourceNode = $this->createNode($source); 278 | $targetNode = $this->createNode($target); 279 | $isSuggested = $version === 'suggest'; 280 | 281 | if (!$this->params->getBool('link-version')) { 282 | $version = ''; 283 | } 284 | 285 | if ($source->isMain() && $target->isDirectPackage()) { 286 | $this->graphWrapper->addLink(new Link($sourceNode, $targetNode, $version, Link::THICK)); 287 | } elseif ($source->isMain() && $target->isDirectPackageDev()) { 288 | $this->graphWrapper->addLink(new Link($sourceNode, $targetNode, $version, Link::THICK)); 289 | } elseif ($isSuggested) { 290 | $this->graphWrapper->addLink(new Link($sourceNode, $targetNode, $version, Link::DOTTED)); 291 | } else { 292 | $this->graphWrapper->addLink(new Link($sourceNode, $targetNode, $version, Link::ARROW)); 293 | } 294 | 295 | $this->createdLinks[$pattern] = $version; 296 | } 297 | } 298 | 299 | private function getGraph(Package $package): Graph 300 | { 301 | if ($package->isMain()) { 302 | return $this->graphMain; 303 | } 304 | 305 | if ($package->isPlatform()) { 306 | return $this->graphPlatform; 307 | } 308 | 309 | if ($package->isTag(Package::DIRECT)) { 310 | if ($package->isTag(Package::REQUIRED) || $package->isTag(Package::SUGGEST)) { 311 | return $this->graphRequire; 312 | } 313 | 314 | if ($package->isTag(Package::REQUIRED_DEV)) { 315 | return $this->graphDev; 316 | } 317 | } 318 | 319 | if ($package->isTag(Package::REQUIRED)) { 320 | return $this->graphRequire; 321 | } 322 | 323 | if ($package->isTag(Package::REQUIRED_DEV)) { 324 | return $this->graphDev; 325 | } 326 | 327 | throw new Exception("Can't detect env for package: {$package->getName(false)}"); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | name = \strtolower($name); 46 | 47 | if ( 48 | !\str_contains($this->name, '/') 49 | && ( 50 | \preg_match('#^ext-[a-z\d]*#', $this->name) > 0 51 | || \preg_match('#^lib-[a-z\d]*#', $this->name) > 0 52 | ) 53 | ) { 54 | $this->addTags([self::EXT, self::HAS_META]); 55 | 56 | if ( 57 | \extension_loaded($this->name) 58 | || \extension_loaded(\str_replace(['ext-', 'lib-'], '', $this->name)) 59 | ) { 60 | $this->addTags([self::INSTALLED]); 61 | } 62 | } 63 | 64 | if ( 65 | $vendorDir !== null 66 | && $vendorDir !== '' 67 | && (\is_dir("{$vendorDir}/{$this->name}") || \is_dir("{$vendorDir}/{$name}")) 68 | ) { 69 | $this->addTags([self::INSTALLED]); 70 | } 71 | } 72 | 73 | public function setVersion(string $version): self 74 | { 75 | if ($version !== '') { 76 | $this->version = \strtolower($version); 77 | } 78 | 79 | return $this; 80 | } 81 | 82 | public function addRequire(array $required): self 83 | { 84 | $this->required = \array_merge($this->required, $required); 85 | 86 | return $this; 87 | } 88 | 89 | public function addRequireDev(array $requiredDev): self 90 | { 91 | $this->requiredDev = \array_merge($this->requiredDev, $requiredDev); 92 | 93 | return $this; 94 | } 95 | 96 | public function addSuggest(array $suggest): self 97 | { 98 | $this->suggests = \array_merge($this->suggests, $suggest); 99 | 100 | return $this; 101 | } 102 | 103 | public function addTags(array $tags): self 104 | { 105 | $this->tags = \array_unique(\array_merge($this->tags, $tags)); 106 | 107 | return $this; 108 | } 109 | 110 | public function isTag(string $tag): bool 111 | { 112 | return \in_array($tag, $this->tags, true); 113 | } 114 | 115 | public function isDirectPackage(): bool 116 | { 117 | return $this->isTag(self::DIRECT) && $this->isTag(self::REQUIRED); 118 | } 119 | 120 | public function isDirectPackageDev(): bool 121 | { 122 | return $this->isTag(self::DIRECT) && $this->isTag(self::REQUIRED_DEV); 123 | } 124 | 125 | public function isPlatform(): bool 126 | { 127 | return !$this->isMain() && ($this->isTag(self::PHP) || $this->isTag(self::EXT)); 128 | } 129 | 130 | public function isPhp(): bool 131 | { 132 | return $this->isTag(self::PHP); 133 | } 134 | 135 | public function isPhpExt(): bool 136 | { 137 | return $this->isTag(self::EXT); 138 | } 139 | 140 | public function isMain(): bool 141 | { 142 | return $this->isTag(self::MAIN); 143 | } 144 | 145 | public function getName(bool $addVersion = true): string 146 | { 147 | $name = \strtolower(\trim($this->name)); 148 | 149 | if ($name === 'php') { 150 | $name = 'PHP'; 151 | } 152 | 153 | $prefixNoMeta = ''; 154 | if (!$this->isTag(self::HAS_META)) { 155 | $prefixNoMeta = '* '; 156 | } 157 | 158 | if (!$addVersion) { 159 | $result = $name; 160 | } else { 161 | $result = $this->version !== '' && $this->version !== '*' 162 | ? "{$name}@{$this->version}" 163 | : $name; 164 | } 165 | 166 | return $prefixNoMeta . $result; 167 | } 168 | 169 | public function getRequired(): array 170 | { 171 | return $this->required; 172 | } 173 | 174 | public function getRequiredDev(): array 175 | { 176 | return $this->requiredDev; 177 | } 178 | 179 | public function getSuggested(): array 180 | { 181 | return $this->suggests; 182 | } 183 | 184 | public function getId(): string 185 | { 186 | return self::alias($this->getName(false)); 187 | } 188 | 189 | public static function alias(string $string): string 190 | { 191 | $string = \strip_tags($string); 192 | 193 | return \str_replace(['/', '-', 'graph', '(', ')', ' ', '*'], ['__', '_', 'g_raph', '', '', '', ''], $string); 194 | } 195 | } 196 | --------------------------------------------------------------------------------