diff --git a/src/Composer/Command/BumpCommand.php b/src/Composer/Command/BumpCommand.php index 5cd7a0f20..dc3df1e12 100644 --- a/src/Composer/Command/BumpCommand.php +++ b/src/Composer/Command/BumpCommand.php @@ -217,14 +217,7 @@ EOT } if (!$dryRun && $composer->getLocker()->isLocked() && $changeCount > 0) { - $contents = file_get_contents($composerJson->getPath()); - if (false === $contents) { - throw new \RuntimeException('Unable to read '.$composerJson->getPath().' contents to update the lock file hash.'); - } - $lock = new JsonFile(Factory::getLockFile($composerJsonPath)); - $lockData = $lock->read(); - $lockData['content-hash'] = Locker::getContentHash($contents); - $lock->write($lockData); + $composer->getLocker()->updateHash($composerJson); } if ($dryRun && $changeCount > 0) { diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 6f1b9ea7c..92d0a77b5 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -555,27 +555,14 @@ EOT if (!$dryRun) { $this->updateFile($this->json, $requirements, $requireKey, $removeKey, $sortPackages); if ($locker->isLocked()) { - $contents = file_get_contents($this->json->getPath()); - if (false === $contents) { - throw new \RuntimeException('Unable to read '.$this->json->getPath().' contents to update the lock file hash.'); - } - $lockFile = Factory::getLockFile($this->json->getPath()); - if (file_exists($lockFile)) { - $stabilityFlags = RootPackageLoader::extractStabilityFlags($requirements, $composer->getPackage()->getMinimumStability(), []); - - $lockMtime = filemtime($lockFile); - $lock = new JsonFile($lockFile); - $lockData = $lock->read(); - $lockData['content-hash'] = Locker::getContentHash($contents); + $stabilityFlags = RootPackageLoader::extractStabilityFlags($requirements, $composer->getPackage()->getMinimumStability(), []); + $locker->updateHash($this->json, function (array $lockData) use ($stabilityFlags) { foreach ($stabilityFlags as $packageName => $flag) { $lockData['stability-flags'][$packageName] = $flag; } - ksort($lockData['stability-flags']); - $lock->write($lockData); - if (is_int($lockMtime)) { - @touch($lockFile, $lockMtime); - } - } + + return $lockData; + }); } } diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 5b5ec466c..03f598491 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -372,31 +372,28 @@ class Locker 'Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies', 'This file is @gener'.'ated automatically', ], 'content-hash' => $this->contentHash, - 'packages' => null, + 'packages' => $this->lockPackages($packages), 'packages-dev' => null, 'aliases' => $aliases, 'minimum-stability' => $minimumStability, - 'stability-flags' => \count($stabilityFlags) > 0 ? $stabilityFlags : new \stdClass, + 'stability-flags' => $stabilityFlags, 'prefer-stable' => $preferStable, 'prefer-lowest' => $preferLowest, ]; - if (is_array($lock['stability-flags'])) { - ksort($lock['stability-flags']); - } - - $lock['packages'] = $this->lockPackages($packages); if (null !== $devPackages) { $lock['packages-dev'] = $this->lockPackages($devPackages); } - $lock['platform'] = \count($platformReqs) > 0 ? $platformReqs : new \stdClass; - $lock['platform-dev'] = \count($platformDevReqs) > 0 ? $platformDevReqs : new \stdClass; + $lock['platform'] = $platformReqs; + $lock['platform-dev'] = $platformDevReqs; if (\count($platformOverrides) > 0) { $lock['platform-overrides'] = $platformOverrides; } $lock['plugin-api-version'] = PluginInterface::PLUGIN_API_VERSION; + $lock = $this->fixupJsonDataType($lock); + try { $isLocked = $this->isLocked(); } catch (ParsingException $e) { @@ -418,6 +415,60 @@ class Locker return false; } + /** + * Updates the lock file's hash in-place from a given composer.json's JsonFile + * + * This does not reload or require any packages, and retains the filemtime of the lock file. + * + * Use this only to update the lock file hash after updating a composer.json in ways that are guaranteed NOT to impact the dependency resolution. + * + * This is a risky method, use carefully. + * + * @param (callable(array): array)|null $dataProcessor Receives the lock data and can process it before it gets written to disk + */ + public function updateHash(JsonFile $composerJson, ?callable $dataProcessor = null): void + { + $contents = file_get_contents($composerJson->getPath()); + if (false === $contents) { + throw new \RuntimeException('Unable to read '.$composerJson->getPath().' contents to update the lock file hash.'); + } + + $lockMtime = filemtime($this->lockFile->getPath()); + $lockData = $this->lockFile->read(); + $lockData['content-hash'] = Locker::getContentHash($contents); + if ($dataProcessor !== null) { + $lockData = $dataProcessor($lockData); + } + + $this->lockFile->write($this->fixupJsonDataType($lockData)); + $this->lockDataCache = null; + $this->virtualFileWritten = false; + if (is_int($lockMtime)) { + @touch($this->lockFile->getPath(), $lockMtime); + } + } + + /** + * Ensures correct data types and ordering for the JSON lock format + * + * @param array $lockData + * @return array + */ + private function fixupJsonDataType(array $lockData): array + { + foreach (['stability-flags', 'platform', 'platform-dev'] as $key) { + if (isset($lockData[$key]) && is_array($lockData[$key]) && \count($lockData[$key]) === 0) { + $lockData[$key] = new \stdClass(); + } + } + + if (is_array($lockData['stability-flags'])) { + ksort($lockData['stability-flags']); + } + + return $lockData; + } + /** * @param PackageInterface[] $packages *