Skip to content

Commit 63faff0

Browse files
authored
Build packages list with their dependencies and depth in mind (#24)
1 parent 72f443c commit 63faff0

11 files changed

Lines changed: 370 additions & 11 deletions

File tree

src/ComposerEventHandler.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use Composer\Installer\PackageEvent;
1414
use Composer\Installer\PackageEvents;
1515
use Composer\IO\IOInterface;
16-
use Composer\Package\CompletePackage;
1716
use Composer\Package\PackageInterface;
1817
use Composer\Plugin\PluginInterface;
1918
use Composer\Script\Event;
@@ -22,7 +21,7 @@
2221
use Symfony\Component\Console\Output\ConsoleOutput;
2322
use Yiisoft\VarDumper\VarDumper;
2423

25-
use function count;
24+
use function array_key_exists;
2625
use function dirname;
2726
use function in_array;
2827

@@ -100,10 +99,7 @@ public function onPostAutoloadDump(Event $event): void
10099
$outputDirectory = $this->getPluginOutputDirectory($rootPackage);
101100
$this->ensureDirectoryExists($outputDirectory);
102101

103-
$allPackages = array_filter(
104-
$composer->getRepositoryManager()->getLocalRepository()->getPackages(),
105-
static fn ($package) => $package instanceof CompletePackage
106-
);
102+
$allPackages = (new PackagesListBuilder($composer))->build();
107103
$packagesForCheck = array_map(
108104
static fn (PackageInterface $package) => $package->getPrettyName(),
109105
$this->updatedPackages
@@ -169,13 +165,12 @@ public function onPostAutoloadDump(Event $event): void
169165

170166
// Append root package config.
171167
foreach ($rootConfig as $group => $files) {
172-
$mergePlan[$group]['/'] = (array)$files;
168+
$mergePlan[$group] = ['/' => (array)$files] +
169+
(array_key_exists($group, $mergePlan) ? $mergePlan[$group] : []);
173170
}
174171

175-
// Reverse package order in groups.
176-
foreach ($mergePlan as $group => $files) {
177-
$mergePlan[$group] = array_reverse($files, true);
178-
}
172+
// Sort groups by alphabetical
173+
ksort($mergePlan);
179174

180175
$packageOptions = $outputDirectory . '/' . self::MERGE_PLAN_FILENAME;
181176
file_put_contents($packageOptions, "<?php\n\ndeclare(strict_types=1);\n\n// Do not edit. Content will be replaced.\nreturn " . VarDumper::create($mergePlan)->export(true) . ";\n");

src/PackagesListBuilder.php

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Config;
6+
7+
use Composer\Composer;
8+
use Composer\Package\CompletePackage;
9+
use Composer\Package\PackageInterface;
10+
11+
use function array_key_exists;
12+
13+
/**
14+
* @internal
15+
*/
16+
final class PackagesListBuilder
17+
{
18+
private Composer $composer;
19+
20+
public function __construct(Composer $composer)
21+
{
22+
$this->composer = $composer;
23+
}
24+
25+
/**
26+
* @return CompletePackage[]
27+
*
28+
* @psalm-return array<string, CompletePackage>
29+
*/
30+
public function build(): array
31+
{
32+
$allPackages = $this->getAllPackages();
33+
34+
$packageDepths = [];
35+
$this->calculatePackageDepths($allPackages, $packageDepths, 0, $this->composer->getPackage(), true);
36+
37+
$result = [];
38+
foreach ($this->getSortedPackageNames($packageDepths) as $name) {
39+
if (array_key_exists($name, $allPackages)) {
40+
$result[$name] = $allPackages[$name];
41+
}
42+
}
43+
44+
return $result;
45+
}
46+
47+
/**
48+
* Get package names stable sorted by depth
49+
*
50+
* @psalm-param array<string, int> $packageDepths
51+
*
52+
* @return string[]
53+
*/
54+
private function getSortedPackageNames(array $packageDepths): array
55+
{
56+
$n = 0;
57+
foreach ($packageDepths as $name => $depth) {
58+
$packageDepths[$name] = [$depth, ++$n];
59+
}
60+
61+
/** @psalm-var array<string, array{0:int,1:int}> $packageDepths */
62+
63+
uasort($packageDepths, static function (array $a, array $b) {
64+
$result = $a[0] <=> $b[0];
65+
return $result === 0 ? $a[1] <=> $b[1] : $result;
66+
});
67+
68+
return array_keys($packageDepths);
69+
}
70+
71+
/**
72+
* @param CompletePackage[] $allPackages
73+
*
74+
* @psalm-param array<string, CompletePackage> $allPackages
75+
* @psalm-param array<string, int> $packageDepths
76+
*/
77+
private function calculatePackageDepths(
78+
array $allPackages,
79+
array &$packageDepths,
80+
int $depth,
81+
PackageInterface $package,
82+
bool $includingDev
83+
): void {
84+
$name = $package->getPrettyName();
85+
86+
$packageProcessed = array_key_exists($name, $packageDepths);
87+
88+
if (!$packageProcessed || $packageDepths[$name] < $depth) {
89+
$packageDepths[$name] = $depth;
90+
}
91+
92+
// Prevent infinite loop in case of circular dependencies
93+
if ($packageProcessed) {
94+
return;
95+
}
96+
97+
++$depth;
98+
99+
$dependencies = $includingDev
100+
? array_keys($package->getRequires())
101+
: [...array_keys($package->getRequires()), ...array_keys($package->getDevRequires())];
102+
103+
foreach ($dependencies as $dependency) {
104+
if (array_key_exists($dependency, $allPackages)) {
105+
$this->calculatePackageDepths($allPackages, $packageDepths, $depth, $allPackages[$dependency], false);
106+
}
107+
}
108+
}
109+
110+
/**
111+
* @return CompletePackage[]
112+
*
113+
* @psalm-return array<string, CompletePackage>
114+
*/
115+
private function getAllPackages(): array
116+
{
117+
$packages = $this->composer->getRepositoryManager()->getLocalRepository()->getPackages();
118+
119+
$result = [];
120+
foreach ($packages as $package) {
121+
if (!$package instanceof CompletePackage) {
122+
continue;
123+
}
124+
$result[$package->getPrettyName()] = $package;
125+
}
126+
127+
return $result;
128+
}
129+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Config\Tests\Integration;
6+
7+
final class PackagesListBuilderTest extends ComposerTest
8+
{
9+
protected function getStartComposerConfig(): array
10+
{
11+
$packages = [
12+
'a',
13+
'ba',
14+
'c',
15+
];
16+
17+
$repositories = [
18+
[
19+
'type' => 'path',
20+
'url' => '../../',
21+
],
22+
];
23+
foreach ($packages as $package) {
24+
$repositories[] = [
25+
'type' => 'path',
26+
'url' => '../Packages/test/' . $package,
27+
'options' => [
28+
'symlink' => false,
29+
],
30+
];
31+
}
32+
33+
return [
34+
'name' => 'yiisoft/testpackage',
35+
'type' => 'library',
36+
'minimum-stability' => 'dev',
37+
'require' => [
38+
'yiisoft/config' => '*',
39+
],
40+
'repositories' => $repositories,
41+
'extra' => [
42+
'config-plugin' => [
43+
'params' => [
44+
'config/params.php',
45+
'?config/params-local.php',
46+
],
47+
'web' => ['config/web.php'],
48+
],
49+
],
50+
];
51+
}
52+
53+
public function testBase(): void
54+
{
55+
$this->assertMergePlan([
56+
'params' => [
57+
'/' => [
58+
'config/params.php',
59+
'?config/params-local.php',
60+
],
61+
],
62+
'web' => [
63+
'/' => [
64+
'config/web.php',
65+
],
66+
],
67+
]);
68+
69+
$this->execComposer('require test/a');
70+
$this->assertMergePlan([
71+
'params' => [
72+
'/' => [
73+
'config/params.php',
74+
'?config/params-local.php',
75+
],
76+
'test/a' => [
77+
'config/params.php',
78+
],
79+
],
80+
'web' => [
81+
'/' => [
82+
'config/web.php',
83+
],
84+
'test/a' => [
85+
'config/web.php',
86+
],
87+
],
88+
]);
89+
90+
$this->execComposer('require test/ba');
91+
$this->assertMergePlan([
92+
'params' => [
93+
'/' => [
94+
'config/params.php',
95+
'?config/params-local.php',
96+
],
97+
'test/a' => [
98+
'config/params.php',
99+
],
100+
],
101+
'web' => [
102+
'/' => [
103+
'config/web.php',
104+
],
105+
'test/ba' => [
106+
'config/web.php',
107+
],
108+
'test/a' => [
109+
'config/web.php',
110+
],
111+
],
112+
]);
113+
114+
$this->execComposer('require test/c');
115+
$this->assertMergePlan([
116+
'params' => [
117+
'/' => [
118+
'config/params.php',
119+
'?config/params-local.php',
120+
],
121+
'test/c' => [
122+
'config/params.php',
123+
],
124+
'test/a' => [
125+
'config/params.php',
126+
],
127+
],
128+
'web' => [
129+
'/' => [
130+
'config/web.php',
131+
],
132+
'test/ba' => [
133+
'config/web.php',
134+
],
135+
'test/c' => [
136+
'config/web.php',
137+
],
138+
'test/a' => [
139+
'config/web.php',
140+
],
141+
],
142+
]);
143+
}
144+
145+
private function assertMergePlan(array $mergePlan): void
146+
{
147+
$this->assertSame($mergePlan, require $this->workingDirectory . '/config/packages/merge_plan.php');
148+
}
149+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "test/a",
3+
"version": "1.0.0",
4+
"autoload": {
5+
"psr-4": {
6+
"Test\\Yii\\View\\": "src"
7+
}
8+
},
9+
"extra": {
10+
"config-plugin": {
11+
"params": "config/params.php",
12+
"web": "config/web.php"
13+
}
14+
}
15+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
return [];
6+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/** @var array $params */
6+
7+
return [
8+
stdClass::class => stdClass::class,
9+
];
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "test/ba",
3+
"version": "1.0.0",
4+
"autoload": {
5+
"psr-4": {
6+
"Test\\Yii\\View\\": "src"
7+
}
8+
},
9+
"require": {
10+
"test/a": "*"
11+
},
12+
"extra": {
13+
"config-plugin": {
14+
"web": "config/web.php"
15+
}
16+
}
17+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/** @var array $params */
6+
7+
return [
8+
stdClass::class => stdClass::class,
9+
];

0 commit comments

Comments
 (0)