├── 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 | [](https://github.com/JBZoo/Composer-Graph/actions/workflows/main.yml?query=branch%3Amaster) [](https://coveralls.io/github/JBZoo/Composer-Graph?branch=master) [](https://shepherd.dev/github/JBZoo/Composer-Graph) [](https://shepherd.dev/github/JBZoo/Composer-Graph) [](https://www.codefactor.io/repository/github/jbzoo/composer-graph/issues)
4 | [](https://packagist.org/packages/jbzoo/composer-graph/) [](https://packagist.org/packages/jbzoo/composer-graph/stats) [](https://packagist.org/packages/jbzoo/composer-graph/dependents?order_by=downloads) [](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 | 
88 |
89 |
90 |
91 | ### Default output with PHP extensions (modules)
92 | ```shell
93 | php ./vendor/bin/composer-graph --show-ext
94 | ```
95 |
96 | 
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 | 
106 |
107 |
108 |
109 | ### Show suggested packages which are not installed
110 | ```shell
111 | php ./vendor/bin/composer-graph --show-suggests
112 | ```
113 |
114 | 
115 |
116 |
117 |
118 | ### Show dev dependencies
119 | ```shell
120 | php ./vendor/bin/composer-graph --show-dev
121 | ```
122 |
123 | 
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 | 
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 |
--------------------------------------------------------------------------------