Skip to content

Commit 3f5e17a

Browse files
Merge pull request #57 from youwe-petervanderwal/fix/config-installer-should-only-update-config
fix: don't rewrite composer.json empty objects to arrays
2 parents 36927a1 + 89dc219 commit 3f5e17a

File tree

8 files changed

+453
-93
lines changed

8 files changed

+453
-93
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5959
- Dropped support for Laravel and Magento 1.
6060
- Dropped inner dependencies on coding-standard, coding-standard-magento2, and coding-standard-phpstorm packages.
6161

62+
### Fixed
63+
- The Composer Config Installer changed empty objects (e.g. a `"autoload-dev": {"psr-4": {}}`) into an empty array
64+
(for previous example: `"autoload-dev": {"psr-4": []}`) causing an invalid `composer.json` file (followed by other
65+
composer operations not being able to be performed). This is now fixed.
66+
6267
## 2.19.1
6368
### Changed
6469
- `^0.30` restricts updates to only versions within the `0.30.x` range, preventing upgrades to 0.32.0 for

src/ComposerJsonWriter.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Youwe\TestingSuite\Composer;
6+
7+
use Composer\Factory;
8+
use UnexpectedValueException;
9+
10+
class ComposerJsonWriter
11+
{
12+
private readonly string $file;
13+
14+
public function __construct(?string $file = null)
15+
{
16+
$this->file = $file ?? Factory::getComposerFile();
17+
}
18+
19+
public function getContents(): object
20+
{
21+
return json_decode(file_get_contents($this->file), associative: false, flags: JSON_THROW_ON_ERROR);
22+
}
23+
24+
public function setContents(object $contents): void
25+
{
26+
file_put_contents(
27+
$this->file,
28+
json_encode($contents, flags: JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
29+
);
30+
}
31+
32+
public function mergeContents(object|array $settings, bool $overwrite = false): void
33+
{
34+
$this->setContents(
35+
$this->mergeObject($this->getContents(), $settings, $overwrite),
36+
);
37+
}
38+
39+
private function mergeObject(?object $existing, object|array $new, bool $overwrite): object
40+
{
41+
if ($existing === null) {
42+
$existing = (object) [];
43+
}
44+
45+
foreach ((array) $new as $key => $value) {
46+
if (is_array($value) && array_is_list($value)) {
47+
// Merge lists
48+
$existing->{$key} = $this->mergeList($existing->{$key} ?? [], $value);
49+
continue;
50+
}
51+
52+
if (is_object($value) || is_array($value)) {
53+
// Deep merge new config
54+
$existing->{$key} = $this->mergeObject($existing->{$key} ?? null, $value, $overwrite);
55+
continue;
56+
}
57+
58+
if (!$overwrite && isset($existing->{$key})) {
59+
continue;
60+
}
61+
62+
$existing->{$key} = $value;
63+
}
64+
65+
return $existing;
66+
}
67+
68+
private function mergeList(mixed $existing, array $new): array
69+
{
70+
if ($existing !== null && !is_array($existing)) {
71+
throw new UnexpectedValueException('Can\'t merge an array list with ' . get_debug_type($existing));
72+
}
73+
74+
return array_merge($existing ?? [], $new);
75+
}
76+
}

src/Installer/ArchiveExcludeInstaller.php

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99

1010
namespace Youwe\TestingSuite\Composer\Installer;
1111

12-
use Composer\Factory;
1312
use Composer\IO\IOInterface;
14-
use Composer\Json\JsonFile;
1513
use Exception;
1614
use Youwe\FileMapping\FileMappingInterface;
15+
use Youwe\TestingSuite\Composer\ComposerJsonWriter;
1716
use Youwe\TestingSuite\Composer\MappingResolver;
1817

1918
/**
@@ -22,20 +21,7 @@
2221
*/
2322
class ArchiveExcludeInstaller implements InstallerInterface
2423
{
25-
/** @var JsonFile */
26-
private $file;
27-
28-
/** @var MappingResolver */
29-
private $resolver;
30-
31-
/** @var IOInterface */
32-
private $io;
33-
34-
/** @var string */
35-
private $destination;
36-
37-
/** @var array */
38-
private $defaults = [
24+
private const DEFAULTS = [
3925
'/docker-compose.yml',
4026
'/examples',
4127
'/example',
@@ -45,27 +31,27 @@ class ArchiveExcludeInstaller implements InstallerInterface
4531
'/tests',
4632
];
4733

34+
private readonly string $destination;
35+
private readonly array $defaults;
36+
4837
/**
4938
* Constructor.
5039
*
5140
* @param MappingResolver $resolver
52-
* @param IOInterface $io
53-
* @param JsonFile|null $file
54-
* @param string |null $destination
55-
* @param array|null $defaults
41+
* @param IOInterface $io
42+
* @param ComposerJsonWriter $composerJsonWriter
43+
* @param string |null $destination
44+
* @param array|null $defaults
5645
*/
5746
public function __construct(
58-
MappingResolver $resolver,
59-
IOInterface $io,
60-
?JsonFile $file = null,
47+
private readonly MappingResolver $resolver,
48+
private readonly IOInterface $io,
49+
private readonly ComposerJsonWriter $composerJsonWriter,
6150
?string $destination = null,
6251
?array $defaults = null,
6352
) {
64-
$this->resolver = $resolver;
65-
$this->io = $io;
66-
$this->file = $file ?? new JsonFile(Factory::getComposerFile());
6753
$this->destination = $destination ?? getcwd();
68-
$this->defaults = $defaults ?? $this->defaults;
54+
$this->defaults = $defaults ?? self::DEFAULTS;
6955
}
7056

7157
/**
@@ -76,16 +62,16 @@ public function __construct(
7662
*/
7763
public function install(): void
7864
{
79-
$definition = $this->file->read();
80-
$excluded = $definition['archive']['exclude'] ?? [];
65+
$definition = $this->composerJsonWriter->getContents();
66+
$excluded = $definition->archive->exclude ?? [];
8167

8268
$excluded = array_map(
8369
function (string $exclude): string {
8470
return substr($exclude, 0, 1) !== '/'
8571
? '/' . $exclude
8672
: $exclude;
8773
},
88-
$excluded
74+
$excluded,
8975
);
9076

9177
$files = array_merge(
@@ -95,27 +81,37 @@ function (FileMappingInterface $mapping): string {
9581
return '/' . $mapping->getRelativeDestination();
9682
},
9783
iterator_to_array(
98-
$this->resolver->resolve()
84+
$this->resolver->resolve(),
9985
),
100-
)
86+
),
10187
);
10288

89+
$hasChanges = false;
10390
foreach ($files as $file) {
10491
if (
10592
!in_array($file, $excluded)
10693
&& file_exists($this->destination . $file)
10794
) {
10895
$excluded[] = $file;
96+
$hasChanges = true;
10997
$this->io->write(
11098
sprintf(
11199
'<info>Added:</info> %s to archive exclude in composer.json',
112100
$file,
113-
)
101+
),
114102
);
115103
}
116104
}
117105

118-
$definition['archive']['exclude'] = $excluded;
119-
$this->file->write($definition);
106+
if (!$hasChanges) {
107+
return;
108+
}
109+
110+
if (!isset($definition->archive)) {
111+
$definition->archive = (object) [];
112+
}
113+
$definition->archive->exclude = $excluded;
114+
115+
$this->composerJsonWriter->setContents($definition);
120116
}
121117
}

src/Installer/ConfigInstaller.php

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99

1010
namespace Youwe\TestingSuite\Composer\Installer;
1111

12-
use Composer\Factory;
13-
use Composer\Json\JsonFile;
14-
use Seld\JsonLint\ParsingException;
12+
use Youwe\TestingSuite\Composer\ComposerJsonWriter;
1513
use Youwe\TestingSuite\Composer\ConfigResolver;
1614

1715
/**
@@ -20,43 +18,28 @@
2018
*/
2119
class ConfigInstaller implements InstallerInterface
2220
{
23-
/** @var JsonFile */
24-
private $file;
25-
26-
/** @var ConfigResolver */
27-
private $resolver;
28-
2921
/**
3022
* Constructor.
3123
*
3224
* @param ConfigResolver $resolver
33-
* @param JsonFile|null $file
25+
* @param ComposerJsonWriter $composerJsonWriter
3426
*/
3527
public function __construct(
36-
ConfigResolver $resolver,
37-
?JsonFile $file = null,
28+
private readonly ConfigResolver $resolver,
29+
private readonly ComposerJsonWriter $composerJsonWriter,
3830
) {
39-
$this->resolver = $resolver;
40-
$this->file = $file ?? new JsonFile(Factory::getComposerFile());
4131
}
4232

4333
/**
4434
* Install.
4535
*
4636
* @return void
47-
* @throws ParsingException
4837
*/
4938
public function install(): void
5039
{
51-
$definition = $this->file->read();
52-
$config = $definition['config'] ?? [];
53-
54-
$config = array_replace_recursive(
55-
$this->resolver->resolve(),
56-
$config,
40+
$this->composerJsonWriter->mergeContents(
41+
settings: ['config' => $this->resolver->resolve()],
42+
overwrite: false,
5743
);
58-
59-
$definition['config'] = $config;
60-
$this->file->write($definition);
6144
}
6245
}

src/installers.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
declare(strict_types=1);
99

10+
use Youwe\TestingSuite\Composer\ComposerJsonWriter;
1011
use Youwe\TestingSuite\Composer\ConfigResolver;
1112
use Youwe\TestingSuite\Composer\Installer\ArchiveExcludeInstaller;
1213
use Youwe\TestingSuite\Composer\Installer\ConfigInstaller;
@@ -23,10 +24,11 @@
2324
$typeResolver = new ProjectTypeResolver($composer);
2425
$mappingResolver = new MappingResolver($typeResolver);
2526
$configResolver = new ConfigResolver($typeResolver);
27+
$composerJsonWriter = new ComposerJsonWriter();
2628

2729
return [
2830
new FilesInstaller($mappingResolver, $io),
29-
new ArchiveExcludeInstaller($mappingResolver, $io),
31+
new ArchiveExcludeInstaller($mappingResolver, $io, $composerJsonWriter),
3032
new PackagesInstaller($composer, $typeResolver, $io),
31-
new ConfigInstaller($configResolver),
33+
new ConfigInstaller($configResolver, $composerJsonWriter),
3234
];

0 commit comments

Comments
 (0)