├── .github ├── FUNDING.yml └── workflows │ └── auto_closer.yaml ├── LICENSE ├── composer.json ├── config └── config.php └── src ├── ComposerJsonFactory.php ├── FileSystem └── JsonFileManager.php ├── Json ├── JsonCleaner.php └── JsonInliner.php ├── Printer └── ComposerJsonPrinter.php ├── Sorter └── ComposerPackageSorter.php └── ValueObject ├── ComposerJson.php ├── ComposerJsonManipulatorConfig.php ├── ComposerJsonSection.php └── Option.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: tomasvotruba 3 | custom: https://www.paypal.me/rectorphp 4 | -------------------------------------------------------------------------------- /.github/workflows/auto_closer.yaml: -------------------------------------------------------------------------------- 1 | name: Auto Closer PR 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: superbrothers/close-pull-request@v3 12 | with: 13 | # Optional. Post a issue comment just before closing a pull request. 14 | comment: | 15 | Hi, thank you for your contribution. 16 | 17 | Unfortunately, this repository is read-only. It's a split from our main monorepo repository. 18 | 19 | We'd like to kindly ask you to move the contribution there - https://github.com/symplify/symplify. 20 | 21 | We'll check it, review it and give you feed back right way. 22 | 23 | Thank you. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | --------------- 3 | 4 | Copyright (c) 2020 Tomas Votruba (https://tomasvotruba.com) 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symplify/composer-json-manipulator", 3 | "description": "Package to load, merge and save composer.json file(s)", 4 | "license": "MIT", 5 | "type": "library", 6 | "require": { 7 | "php": ">=8.1", 8 | "nette/utils": "^3.2", 9 | "symfony/config": "^6.2", 10 | "symfony/dependency-injection": "6.1.*", 11 | "symfony/filesystem": "^6.2", 12 | "symplify/package-builder": "^11.2", 13 | "symplify/symplify-kernel": "^11.2", 14 | "symplify/smart-file-system": "^11.2" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^9.5.26" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Symplify\\ComposerJsonManipulator\\": "src" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Symplify\\ComposerJsonManipulator\\Tests\\": "tests" 27 | } 28 | }, 29 | "extra": { 30 | "branch-alias": { 31 | "dev-main": "11.2-dev" 32 | } 33 | }, 34 | "conflict": { 35 | "symplify/phpstan-rules": "<11.1.18", 36 | "symplify/easy-testing": "<11.1.18", 37 | "symplify/rule-doc-generator-contracts": "<11.1.18", 38 | "symplify/php-config-printer": "<11.1.18", 39 | "symplify/autowire-array-parameter": "<11.1.18", 40 | "symplify/phpstan-extensions": "<11.1.18", 41 | "symplify/rule-doc-generator": "<11.1.18", 42 | "symplify/symfony-static-dumper": "<11.1.18", 43 | "symplify/symplify-kernel": "<9.4.70", 44 | "symplify/config-transformer": "<11.1.18", 45 | "symplify/coding-standard": "<11.1.18", 46 | "symplify/easy-parallel": "<11.1.18", 47 | "symplify/easy-ci": "<11.1.18", 48 | "symplify/monorepo-builder": "<11.1.18", 49 | "symplify/vendor-patches": "<11.1.18", 50 | "symplify/easy-coding-standard": "<11.1.18" 51 | }, 52 | "minimum-stability": "dev", 53 | "prefer-stable": true 54 | } 55 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | parameters(); 16 | 17 | $parameters->set(Option::INLINE_SECTIONS, ['keywords']); 18 | 19 | $services = $containerConfigurator->services(); 20 | 21 | $services->defaults() 22 | ->public() 23 | ->autowire(); 24 | 25 | $services->load('Symplify\ComposerJsonManipulator\\', __DIR__ . '/../src'); 26 | 27 | $services->set(SmartFileSystem::class); 28 | $services->set(PrivatesCaller::class); 29 | 30 | $services->set(ParameterProvider::class) 31 | ->args([service('service_container')]); 32 | 33 | $services->set(SymfonyStyleFactory::class); 34 | $services->set(SymfonyStyle::class) 35 | ->factory([service(SymfonyStyleFactory::class), 'create']); 36 | }; 37 | -------------------------------------------------------------------------------- /src/ComposerJsonFactory.php: -------------------------------------------------------------------------------- 1 | createFromArray($jsonArray); 28 | } 29 | 30 | public function createFromFileInfo(SmartFileInfo $smartFileInfo): ComposerJson 31 | { 32 | $jsonArray = $this->jsonFileManager->loadFromFilePath($smartFileInfo->getRealPath()); 33 | 34 | $composerJson = $this->createFromArray($jsonArray); 35 | $composerJson->setOriginalFileInfo($smartFileInfo); 36 | 37 | return $composerJson; 38 | } 39 | 40 | public function createFromFilePath(string $filePath): ComposerJson 41 | { 42 | $jsonArray = $this->jsonFileManager->loadFromFilePath($filePath); 43 | 44 | $composerJson = $this->createFromArray($jsonArray); 45 | $fileInfo = new SmartFileInfo($filePath); 46 | $composerJson->setOriginalFileInfo($fileInfo); 47 | 48 | return $composerJson; 49 | } 50 | 51 | public function createEmpty(): ComposerJson 52 | { 53 | return new ComposerJson(); 54 | } 55 | 56 | /** 57 | * @param mixed[] $jsonArray 58 | */ 59 | public function createFromArray(array $jsonArray): ComposerJson 60 | { 61 | $composerJson = new ComposerJson(); 62 | 63 | if (isset($jsonArray[ComposerJsonSection::CONFIG])) { 64 | $composerJson->setConfig($jsonArray[ComposerJsonSection::CONFIG]); 65 | } 66 | 67 | if (isset($jsonArray[ComposerJsonSection::NAME])) { 68 | $composerJson->setName($jsonArray[ComposerJsonSection::NAME]); 69 | } 70 | 71 | if (isset($jsonArray[ComposerJsonSection::TYPE])) { 72 | $composerJson->setType($jsonArray[ComposerJsonSection::TYPE]); 73 | } 74 | 75 | if (isset($jsonArray[ComposerJsonSection::AUTHORS])) { 76 | $composerJson->setAuthors($jsonArray[ComposerJsonSection::AUTHORS]); 77 | } 78 | 79 | if (isset($jsonArray[ComposerJsonSection::DESCRIPTION])) { 80 | $composerJson->setDescription($jsonArray[ComposerJsonSection::DESCRIPTION]); 81 | } 82 | 83 | if (isset($jsonArray[ComposerJsonSection::KEYWORDS])) { 84 | $composerJson->setKeywords($jsonArray[ComposerJsonSection::KEYWORDS]); 85 | } 86 | 87 | if (isset($jsonArray[ComposerJsonSection::HOMEPAGE])) { 88 | $composerJson->setHomepage($jsonArray[ComposerJsonSection::HOMEPAGE]); 89 | } 90 | 91 | if (isset($jsonArray[ComposerJsonSection::LICENSE])) { 92 | $composerJson->setLicense($jsonArray[ComposerJsonSection::LICENSE]); 93 | } 94 | 95 | if (isset($jsonArray[ComposerJsonSection::BIN])) { 96 | $composerJson->setBin($jsonArray[ComposerJsonSection::BIN]); 97 | } 98 | 99 | if (isset($jsonArray[ComposerJsonSection::REQUIRE])) { 100 | $composerJson->setRequire($jsonArray[ComposerJsonSection::REQUIRE]); 101 | } 102 | 103 | if (isset($jsonArray[ComposerJsonSection::REQUIRE_DEV])) { 104 | $composerJson->setRequireDev($jsonArray[ComposerJsonSection::REQUIRE_DEV]); 105 | } 106 | 107 | if (isset($jsonArray[ComposerJsonSection::AUTOLOAD])) { 108 | $composerJson->setAutoload($jsonArray[ComposerJsonSection::AUTOLOAD]); 109 | } 110 | 111 | if (isset($jsonArray[ComposerJsonSection::AUTOLOAD_DEV])) { 112 | $composerJson->setAutoloadDev($jsonArray[ComposerJsonSection::AUTOLOAD_DEV]); 113 | } 114 | 115 | if (isset($jsonArray[ComposerJsonSection::REPLACE])) { 116 | $composerJson->setReplace($jsonArray[ComposerJsonSection::REPLACE]); 117 | } 118 | 119 | if (isset($jsonArray[ComposerJsonSection::EXTRA])) { 120 | $composerJson->setExtra($jsonArray[ComposerJsonSection::EXTRA]); 121 | } 122 | 123 | if (isset($jsonArray[ComposerJsonSection::SCRIPTS])) { 124 | $composerJson->setScripts($jsonArray[ComposerJsonSection::SCRIPTS]); 125 | } 126 | 127 | if (isset($jsonArray[ComposerJsonSection::SCRIPTS_DESCRIPTIONS])) { 128 | $composerJson->setScriptsDescriptions($jsonArray[ComposerJsonSection::SCRIPTS_DESCRIPTIONS]); 129 | } 130 | 131 | if (isset($jsonArray[ComposerJsonSection::SUGGEST])) { 132 | $composerJson->setSuggest($jsonArray[ComposerJsonSection::SUGGEST]); 133 | } 134 | 135 | if (isset($jsonArray[ComposerJsonSection::MINIMUM_STABILITY])) { 136 | $composerJson->setMinimumStability($jsonArray[ComposerJsonSection::MINIMUM_STABILITY]); 137 | } 138 | 139 | if (isset($jsonArray[ComposerJsonSection::PREFER_STABLE])) { 140 | $composerJson->setPreferStable($jsonArray[ComposerJsonSection::PREFER_STABLE]); 141 | } 142 | 143 | if (isset($jsonArray[ComposerJsonSection::CONFLICT])) { 144 | $composerJson->setConflicts($jsonArray[ComposerJsonSection::CONFLICT]); 145 | } 146 | 147 | if (isset($jsonArray[ComposerJsonSection::REPOSITORIES])) { 148 | $composerJson->setRepositories($jsonArray[ComposerJsonSection::REPOSITORIES]); 149 | } 150 | 151 | if (isset($jsonArray[ComposerJsonSection::VERSION])) { 152 | $composerJson->setVersion($jsonArray[ComposerJsonSection::VERSION]); 153 | } 154 | 155 | if (isset($jsonArray[ComposerJsonSection::PROVIDE])) { 156 | $composerJson->setProvide($jsonArray[ComposerJsonSection::PROVIDE]); 157 | } 158 | 159 | $orderedKeys = array_keys($jsonArray); 160 | $composerJson->setOrderedKeys($orderedKeys); 161 | 162 | return $composerJson; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/FileSystem/JsonFileManager.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | private array $cachedJSONFiles = []; 24 | 25 | public function __construct( 26 | private SmartFileSystem $smartFileSystem, 27 | private JsonCleaner $jsonCleaner, 28 | private JsonInliner $jsonInliner 29 | ) { 30 | } 31 | 32 | /** 33 | * @return mixed[] 34 | */ 35 | public function loadFromFileInfo(SmartFileInfo $smartFileInfo): array 36 | { 37 | $realPath = $smartFileInfo->getRealPath(); 38 | if (! isset($this->cachedJSONFiles[$realPath])) { 39 | $this->cachedJSONFiles[$realPath] = Json::decode($smartFileInfo->getContents(), Json::FORCE_ARRAY); 40 | } 41 | 42 | return $this->cachedJSONFiles[$realPath]; 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function loadFromFilePath(string $filePath): array 49 | { 50 | $fileContent = $this->smartFileSystem->readFile($filePath); 51 | 52 | return Json::decode($fileContent, Json::FORCE_ARRAY); 53 | } 54 | 55 | /** 56 | * @param mixed[] $json 57 | */ 58 | public function printJsonToFileInfoAndReturn(array $json, SmartFileInfo $smartFileInfo): string 59 | { 60 | $jsonString = $this->encodeJsonToFileContent($json); 61 | $this->printJsonStringToSmartFileInfo($smartFileInfo, $jsonString); 62 | 63 | return $jsonString; 64 | } 65 | 66 | /** 67 | * @param mixed[] $json 68 | */ 69 | public function printJsonToFileInfo(array $json, SmartFileInfo $smartFileInfo): void 70 | { 71 | $jsonString = $this->encodeJsonToFileContent($json); 72 | $this->printJsonStringToSmartFileInfo($smartFileInfo, $jsonString); 73 | } 74 | 75 | public function printComposerJsonToFilePath(ComposerJson $composerJson, string $filePath): void 76 | { 77 | $jsonString = $this->encodeJsonToFileContent($composerJson->getJsonArray()); 78 | $this->smartFileSystem->dumpFile($filePath, $jsonString); 79 | } 80 | 81 | /** 82 | * @param mixed[] $json 83 | */ 84 | public function encodeJsonToFileContent(array $json): string 85 | { 86 | // Empty arrays may lead to bad encoding since we can't be sure whether they need to be arrays or objects. 87 | $json = $this->jsonCleaner->removeEmptyKeysFromJsonArray($json); 88 | $jsonContent = Json::encode($json, Json::PRETTY) . StaticEolConfiguration::getEolChar(); 89 | 90 | return $this->jsonInliner->inlineSections($jsonContent); 91 | } 92 | 93 | private function printJsonStringToSmartFileInfo(SmartFileInfo $smartFileInfo, string $jsonString): void 94 | { 95 | $this->smartFileSystem->dumpFile($smartFileInfo->getPathname(), $jsonString); 96 | 97 | $realPath = $smartFileInfo->getRealPath(); 98 | unset($this->cachedJSONFiles[$realPath]); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Json/JsonCleaner.php: -------------------------------------------------------------------------------- 1 | $data 11 | * @return array 12 | */ 13 | public function removeEmptyKeysFromJsonArray(array $data): array 14 | { 15 | foreach ($data as $key => $value) { 16 | if (! is_array($value)) { 17 | continue; 18 | } 19 | 20 | if ($value === []) { 21 | unset($data[$key]); 22 | } else { 23 | $data[$key] = $this->removeEmptyKeysFromJsonArray($value); 24 | } 25 | } 26 | 27 | return $data; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Json/JsonInliner.php: -------------------------------------------------------------------------------- 1 | parameterProvider->hasParameter(Option::INLINE_SECTIONS)) { 27 | return $jsonContent; 28 | } 29 | 30 | $inlineSections = $this->parameterProvider->provideArrayParameter(Option::INLINE_SECTIONS); 31 | 32 | foreach ($inlineSections as $inlineSection) { 33 | $pattern = '#("' . preg_quote($inlineSection, '#') . '": )\[(.*?)\](,)#ms'; 34 | 35 | $jsonContent = Strings::replace($jsonContent, $pattern, static function (array $match): string { 36 | $inlined = Strings::replace($match[2], self::SPACE_REGEX, ' '); 37 | $inlined = trim($inlined); 38 | $inlined = '[' . $inlined . ']'; 39 | return $match[1] . $inlined . $match[3]; 40 | }); 41 | } 42 | 43 | return $jsonContent; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Printer/ComposerJsonPrinter.php: -------------------------------------------------------------------------------- 1 | jsonFileManager->encodeJsonToFileContent($composerJson->getJsonArray()); 24 | } 25 | 26 | public function print(ComposerJson $composerJson, string | SmartFileInfo $targetFile): void 27 | { 28 | if (is_string($targetFile)) { 29 | $this->jsonFileManager->printComposerJsonToFilePath($composerJson, $targetFile); 30 | return; 31 | } 32 | 33 | $this->jsonFileManager->printJsonToFileInfo($composerJson->getJsonArray(), $targetFile); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Sorter/ComposerPackageSorter.php: -------------------------------------------------------------------------------- 1 | php|hhvm|ext|lib|\D)#'; 27 | 28 | /** 29 | * Sorts packages by importance (platform packages first, then PHP dependencies) and alphabetically. 30 | * 31 | * @link https://getcomposer.org/doc/02-libraries.md#platform-packages 32 | * 33 | * @param array $packages 34 | * @return array 35 | */ 36 | public function sortPackages(array $packages): array 37 | { 38 | uksort( 39 | $packages, 40 | fn (string $firstPackageName, string $secondPackageName): int => $this->createNameWithPriority( 41 | $firstPackageName 42 | ) <=> $this->createNameWithPriority($secondPackageName) 43 | ); 44 | 45 | return $packages; 46 | } 47 | 48 | private function createNameWithPriority(string $requirementName): string 49 | { 50 | if ($this->isPlatformPackage($requirementName)) { 51 | return Strings::replace( 52 | $requirementName, 53 | self::REQUIREMENT_TYPE_REGEX, 54 | static function (array $match): string { 55 | $name = $match['name']; 56 | if ($name === 'php') { 57 | return '0-' . $name; 58 | } 59 | 60 | if ($name === 'hhvm') { 61 | return '0-' . $name; 62 | } 63 | 64 | if ($name === 'ext') { 65 | return '1-' . $name; 66 | } 67 | 68 | if ($name === 'lib') { 69 | return '2-' . $name; 70 | } 71 | 72 | return '3-' . $name; 73 | } 74 | ); 75 | } 76 | 77 | return '4-' . $requirementName; 78 | } 79 | 80 | private function isPlatformPackage(string $name): bool 81 | { 82 | return (bool) Strings::match($name, self::PLATFORM_PACKAGE_REGEX); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ValueObject/ComposerJson.php: -------------------------------------------------------------------------------- 1 | 56 | */ 57 | private array $require = []; 58 | 59 | /** 60 | * @var mixed[] 61 | */ 62 | private array $autoload = []; 63 | 64 | /** 65 | * @var mixed[] 66 | */ 67 | private array $extra = []; 68 | 69 | /** 70 | * @var array 71 | */ 72 | private array $requireDev = []; 73 | 74 | /** 75 | * @var mixed[] 76 | */ 77 | private array $autoloadDev = []; 78 | 79 | /** 80 | * @var string[] 81 | */ 82 | private array $orderedKeys = []; 83 | 84 | /** 85 | * @var array 86 | */ 87 | private array $replace = []; 88 | 89 | /** 90 | * @var array 91 | */ 92 | private array $scripts = []; 93 | 94 | /** 95 | * @var mixed[] 96 | */ 97 | private array $config = []; 98 | 99 | private ?SmartFileInfo $fileInfo = null; 100 | 101 | private ComposerPackageSorter $composerPackageSorter; 102 | 103 | /** 104 | * @var array 105 | */ 106 | private array $conflicts = []; 107 | 108 | /** 109 | * @var mixed[] 110 | */ 111 | private array $bin = []; 112 | 113 | private ?string $type = null; 114 | 115 | /** 116 | * @var mixed[] 117 | */ 118 | private array $authors = []; 119 | 120 | /** 121 | * @var array 122 | */ 123 | private array $scriptsDescriptions = []; 124 | 125 | /** 126 | * @var array 127 | */ 128 | private array $suggest = []; 129 | 130 | private ?string $version = null; 131 | 132 | /** 133 | * @var array 134 | */ 135 | private array $provide = []; 136 | 137 | public function __construct() 138 | { 139 | $this->composerPackageSorter = new ComposerPackageSorter(); 140 | } 141 | 142 | public function setOriginalFileInfo(SmartFileInfo $fileInfo): void 143 | { 144 | $this->fileInfo = $fileInfo; 145 | } 146 | 147 | public function setName(string $name): void 148 | { 149 | $this->name = $name; 150 | } 151 | 152 | public function setType(string $type): void 153 | { 154 | $this->type = $type; 155 | } 156 | 157 | /** 158 | * @param array $require 159 | */ 160 | public function setRequire(array $require): void 161 | { 162 | $this->require = $this->sortPackagesIfNeeded($require); 163 | } 164 | 165 | public function getVersion(): ?string 166 | { 167 | return $this->version; 168 | } 169 | 170 | public function setVersion(string $version): void 171 | { 172 | $this->version = $version; 173 | } 174 | 175 | /** 176 | * @return mixed[] 177 | */ 178 | public function getRequire(): array 179 | { 180 | return $this->require; 181 | } 182 | 183 | public function getRequirePhpVersion(): ?string 184 | { 185 | return $this->require[self::PHP] ?? null; 186 | } 187 | 188 | /** 189 | * @return array 190 | */ 191 | public function getRequireDev(): array 192 | { 193 | return $this->requireDev; 194 | } 195 | 196 | /** 197 | * @param array $requireDev 198 | */ 199 | public function setRequireDev(array $requireDev): void 200 | { 201 | $this->requireDev = $this->sortPackagesIfNeeded($requireDev); 202 | } 203 | 204 | /** 205 | * @param string[] $orderedKeys 206 | */ 207 | public function setOrderedKeys(array $orderedKeys): void 208 | { 209 | $this->orderedKeys = $orderedKeys; 210 | } 211 | 212 | /** 213 | * @return string[] 214 | */ 215 | public function getOrderedKeys(): array 216 | { 217 | return $this->orderedKeys; 218 | } 219 | 220 | /** 221 | * @return mixed[] 222 | */ 223 | public function getAutoload(): array 224 | { 225 | return $this->autoload; 226 | } 227 | 228 | /** 229 | * @return string[] 230 | */ 231 | public function getAbsoluteAutoloadDirectories(): array 232 | { 233 | if ($this->fileInfo === null) { 234 | throw new ShouldNotHappenException(); 235 | } 236 | 237 | $autoloadDirectories = $this->getAutoloadDirectories(); 238 | 239 | $absoluteAutoloadDirectories = []; 240 | 241 | foreach ($autoloadDirectories as $autoloadDirectory) { 242 | if (is_file($autoloadDirectory)) { 243 | // skip files 244 | continue; 245 | } 246 | 247 | $absoluteAutoloadDirectories[] = $this->resolveExistingAutoloadDirectory($autoloadDirectory); 248 | } 249 | 250 | return $absoluteAutoloadDirectories; 251 | } 252 | 253 | /** 254 | * @param mixed[] $autoload 255 | */ 256 | public function setAutoload(array $autoload): void 257 | { 258 | $this->autoload = $autoload; 259 | } 260 | 261 | /** 262 | * @return mixed[] 263 | */ 264 | public function getAutoloadDev(): array 265 | { 266 | return $this->autoloadDev; 267 | } 268 | 269 | /** 270 | * @param mixed[] $autoloadDev 271 | */ 272 | public function setAutoloadDev(array $autoloadDev): void 273 | { 274 | $this->autoloadDev = $autoloadDev; 275 | } 276 | 277 | /** 278 | * @return mixed[] 279 | */ 280 | public function getRepositories(): array 281 | { 282 | return $this->repositories; 283 | } 284 | 285 | /** 286 | * @param mixed[] $repositories 287 | */ 288 | public function setRepositories(array $repositories): void 289 | { 290 | $this->repositories = $repositories; 291 | } 292 | 293 | public function setMinimumStability(string $minimumStability): void 294 | { 295 | $this->minimumStability = $minimumStability; 296 | } 297 | 298 | public function removeMinimumStability(): void 299 | { 300 | $this->minimumStability = null; 301 | } 302 | 303 | public function getMinimumStability(): ?string 304 | { 305 | return $this->minimumStability; 306 | } 307 | 308 | public function getPreferStable(): ?bool 309 | { 310 | return $this->preferStable; 311 | } 312 | 313 | public function setPreferStable(bool $preferStable): void 314 | { 315 | $this->preferStable = $preferStable; 316 | } 317 | 318 | public function removePreferStable(): void 319 | { 320 | $this->preferStable = null; 321 | } 322 | 323 | /** 324 | * @return mixed[] 325 | */ 326 | public function getExtra(): array 327 | { 328 | return $this->extra; 329 | } 330 | 331 | /** 332 | * @param mixed[] $extra 333 | */ 334 | public function setExtra(array $extra): void 335 | { 336 | $this->extra = $extra; 337 | } 338 | 339 | public function getName(): ?string 340 | { 341 | return $this->name; 342 | } 343 | 344 | public function getVendorName(): ?string 345 | { 346 | if ($this->name === null) { 347 | return null; 348 | } 349 | 350 | [$vendor] = explode('/', $this->name); 351 | return $vendor; 352 | } 353 | 354 | public function getShortName(): ?string 355 | { 356 | if ($this->name === null) { 357 | return null; 358 | } 359 | 360 | return Strings::after($this->name, '/', -1); 361 | } 362 | 363 | /** 364 | * @return array 365 | */ 366 | public function getReplace(): array 367 | { 368 | return $this->replace; 369 | } 370 | 371 | public function isReplacePackageSet(string $packageName): bool 372 | { 373 | return isset($this->replace[$packageName]); 374 | } 375 | 376 | /** 377 | * @param array $replace 378 | */ 379 | public function setReplace(array $replace): void 380 | { 381 | ksort($replace); 382 | 383 | $this->replace = $replace; 384 | } 385 | 386 | public function setReplacePackage(string $packageName, string $version): void 387 | { 388 | $this->replace[$packageName] = $version; 389 | } 390 | 391 | /** 392 | * @return mixed[] 393 | */ 394 | public function getJsonArray(): array 395 | { 396 | $array = array_filter([ 397 | ComposerJsonSection::NAME => $this->name, 398 | ComposerJsonSection::DESCRIPTION => $this->description, 399 | ComposerJsonSection::KEYWORDS => $this->keywords, 400 | ComposerJsonSection::HOMEPAGE => $this->homepage, 401 | ComposerJsonSection::LICENSE => $this->license, 402 | ComposerJsonSection::AUTHORS => $this->authors, 403 | ComposerJsonSection::TYPE => $this->type, 404 | ComposerJsonSection::REQUIRE => $this->require, 405 | ComposerJsonSection::REQUIRE_DEV => $this->requireDev, 406 | ComposerJsonSection::AUTOLOAD => $this->autoload, 407 | ComposerJsonSection::AUTOLOAD_DEV => $this->autoloadDev, 408 | ComposerJsonSection::REPOSITORIES => $this->repositories, 409 | ComposerJsonSection::EXTRA => $this->extra, 410 | ComposerJsonSection::BIN => $this->bin, 411 | ComposerJsonSection::SCRIPTS => $this->scripts, 412 | ComposerJsonSection::SCRIPTS_DESCRIPTIONS => $this->scriptsDescriptions, 413 | ComposerJsonSection::SUGGEST => $this->suggest, 414 | ComposerJsonSection::CONFIG => $this->config, 415 | ComposerJsonSection::REPLACE => $this->replace, 416 | ComposerJsonSection::CONFLICT => $this->conflicts, 417 | ComposerJsonSection::PROVIDE => $this->provide, 418 | ComposerJsonSection::VERSION => $this->version, 419 | ]); 420 | 421 | if ($this->minimumStability !== null) { 422 | $array[ComposerJsonSection::MINIMUM_STABILITY] = $this->minimumStability; 423 | $this->moveValueToBack(ComposerJsonSection::MINIMUM_STABILITY); 424 | } 425 | 426 | if ($this->preferStable !== null) { 427 | $array[ComposerJsonSection::PREFER_STABLE] = $this->preferStable; 428 | $this->moveValueToBack(ComposerJsonSection::PREFER_STABLE); 429 | } 430 | 431 | return $this->sortItemsByOrderedListOfKeys($array, $this->orderedKeys); 432 | } 433 | 434 | /** 435 | * @param array $scripts 436 | */ 437 | public function setScripts(array $scripts): void 438 | { 439 | $this->scripts = $scripts; 440 | } 441 | 442 | /** 443 | * @param mixed[] $config 444 | */ 445 | public function setConfig(array $config): void 446 | { 447 | $this->config = $config; 448 | } 449 | 450 | /** 451 | * @return mixed[] 452 | */ 453 | public function getConfig(): array 454 | { 455 | return $this->config; 456 | } 457 | 458 | public function setDescription(string $description): void 459 | { 460 | $this->description = $description; 461 | } 462 | 463 | public function getDescription(): ?string 464 | { 465 | return $this->description; 466 | } 467 | 468 | /** 469 | * @param string[] $keywords 470 | */ 471 | public function setKeywords(array $keywords): void 472 | { 473 | $this->keywords = $keywords; 474 | } 475 | 476 | /** 477 | * @return string[] 478 | */ 479 | public function getKeywords(): array 480 | { 481 | return $this->keywords; 482 | } 483 | 484 | public function setHomepage(string $homepage): void 485 | { 486 | $this->homepage = $homepage; 487 | } 488 | 489 | public function getHomepage(): ?string 490 | { 491 | return $this->homepage; 492 | } 493 | 494 | /** 495 | * @param string|string[]|null $license 496 | */ 497 | public function setLicense(string | array | null $license): void 498 | { 499 | $this->license = $license; 500 | } 501 | 502 | /** 503 | * @return string|string[]|null 504 | */ 505 | public function getLicense(): string|array|null 506 | { 507 | return $this->license; 508 | } 509 | 510 | /** 511 | * @param mixed[] $authors 512 | */ 513 | public function setAuthors(array $authors): void 514 | { 515 | $this->authors = $authors; 516 | } 517 | 518 | /** 519 | * @return mixed[] 520 | */ 521 | public function getAuthors(): array 522 | { 523 | return $this->authors; 524 | } 525 | 526 | public function hasPackage(string $packageName): bool 527 | { 528 | if ($this->hasRequiredPackage($packageName)) { 529 | return true; 530 | } 531 | 532 | return $this->hasRequiredDevPackage($packageName); 533 | } 534 | 535 | public function hasRequiredPackage(string $packageName): bool 536 | { 537 | return isset($this->require[$packageName]); 538 | } 539 | 540 | public function hasRequiredDevPackage(string $packageName): bool 541 | { 542 | return isset($this->requireDev[$packageName]); 543 | } 544 | 545 | public function addRequiredPackage(string $packageName, string $version): void 546 | { 547 | if (! $this->hasPackage($packageName)) { 548 | $this->require[$packageName] = $version; 549 | $this->require = $this->sortPackagesIfNeeded($this->require); 550 | } 551 | } 552 | 553 | public function addRequiredDevPackage(string $packageName, string $version): void 554 | { 555 | if (! $this->hasPackage($packageName)) { 556 | $this->requireDev[$packageName] = $version; 557 | $this->requireDev = $this->sortPackagesIfNeeded($this->requireDev); 558 | } 559 | } 560 | 561 | public function changePackageVersion(string $packageName, string $version): void 562 | { 563 | if ($this->hasRequiredPackage($packageName)) { 564 | $this->require[$packageName] = $version; 565 | } 566 | 567 | if ($this->hasRequiredDevPackage($packageName)) { 568 | $this->requireDev[$packageName] = $version; 569 | } 570 | } 571 | 572 | public function movePackageToRequire(string $packageName): void 573 | { 574 | if (! $this->hasRequiredDevPackage($packageName)) { 575 | return; 576 | } 577 | 578 | $version = $this->requireDev[$packageName]; 579 | $this->removePackage($packageName); 580 | $this->addRequiredPackage($packageName, $version); 581 | } 582 | 583 | public function movePackageToRequireDev(string $packageName): void 584 | { 585 | if (! $this->hasRequiredPackage($packageName)) { 586 | return; 587 | } 588 | 589 | $version = $this->require[$packageName]; 590 | $this->removePackage($packageName); 591 | $this->addRequiredDevPackage($packageName, $version); 592 | } 593 | 594 | public function removePackage(string $packageName): void 595 | { 596 | unset($this->require[$packageName], $this->requireDev[$packageName]); 597 | } 598 | 599 | public function replacePackage(string $oldPackageName, string $newPackageName, string $targetVersion): void 600 | { 601 | if ($this->hasRequiredPackage($oldPackageName)) { 602 | unset($this->require[$oldPackageName]); 603 | $this->addRequiredPackage($newPackageName, $targetVersion); 604 | } 605 | 606 | if ($this->hasRequiredDevPackage($oldPackageName)) { 607 | unset($this->requireDev[$oldPackageName]); 608 | $this->addRequiredDevPackage($newPackageName, $targetVersion); 609 | } 610 | } 611 | 612 | public function getFileInfo(): ?SmartFileInfo 613 | { 614 | return $this->fileInfo; 615 | } 616 | 617 | /** 618 | * @param array $conflicts 619 | */ 620 | public function setConflicts(array $conflicts): void 621 | { 622 | $this->conflicts = $conflicts; 623 | } 624 | 625 | /** 626 | * @param mixed[] $bin 627 | */ 628 | public function setBin(array $bin): void 629 | { 630 | $this->bin = $bin; 631 | } 632 | 633 | /** 634 | * @return mixed[] 635 | */ 636 | public function getBin(): array 637 | { 638 | return $this->bin; 639 | } 640 | 641 | /** 642 | * @return string[] 643 | */ 644 | public function getPsr4AndClassmapDirectories(): array 645 | { 646 | $psr4Directories = array_values($this->autoload['psr-4'] ?? []); 647 | $classmapDirectories = $this->autoload['classmap'] ?? []; 648 | 649 | return array_merge($psr4Directories, $classmapDirectories); 650 | } 651 | 652 | /** 653 | * @return array 654 | */ 655 | public function getScripts(): array 656 | { 657 | return $this->scripts; 658 | } 659 | 660 | /** 661 | * @return array 662 | */ 663 | public function getScriptsDescriptions(): array 664 | { 665 | return $this->scriptsDescriptions; 666 | } 667 | 668 | /** 669 | * @return array 670 | */ 671 | public function getSuggest(): array 672 | { 673 | return $this->suggest; 674 | } 675 | 676 | /** 677 | * @return string[] 678 | */ 679 | public function getAllClassmaps(): array 680 | { 681 | $autoloadClassmaps = $this->autoload[self::CLASSMAP_KEY] ?? []; 682 | $autoloadDevClassmaps = $this->autoloadDev[self::CLASSMAP_KEY] ?? []; 683 | 684 | return array_merge($autoloadClassmaps, $autoloadDevClassmaps); 685 | } 686 | 687 | /** 688 | * @return array 689 | */ 690 | public function getConflicts(): array 691 | { 692 | return $this->conflicts; 693 | } 694 | 695 | /** 696 | * @api 697 | */ 698 | public function getType(): ?string 699 | { 700 | return $this->type; 701 | } 702 | 703 | /** 704 | * @return string[] 705 | */ 706 | public function getAutoloadDirectories(): array 707 | { 708 | $autoloadDirectories = array_merge( 709 | $this->getPsr4AndClassmapDirectories(), 710 | $this->getPsr4AndClassmapDevDirectories() 711 | ); 712 | 713 | return Arrays::flatten($autoloadDirectories); 714 | } 715 | 716 | /** 717 | * @return string[] 718 | */ 719 | public function getPsr4AndClassmapDevDirectories(): array 720 | { 721 | $psr4Directories = array_values($this->autoloadDev['psr-4'] ?? []); 722 | $classmapDirectories = $this->autoloadDev['classmap'] ?? []; 723 | 724 | return array_merge($psr4Directories, $classmapDirectories); 725 | } 726 | 727 | /** 728 | * @param array $scriptsDescriptions 729 | */ 730 | public function setScriptsDescriptions(array $scriptsDescriptions): void 731 | { 732 | $this->scriptsDescriptions = $scriptsDescriptions; 733 | } 734 | 735 | /** 736 | * @param array $suggest 737 | */ 738 | public function setSuggest(array $suggest): void 739 | { 740 | $this->suggest = $suggest; 741 | } 742 | 743 | /** 744 | * @return string[] 745 | */ 746 | public function getDuplicatedRequirePackages(): array 747 | { 748 | $requiredPackageNames = $this->require; 749 | $requiredDevPackageNames = $this->requireDev; 750 | 751 | return array_intersect($requiredPackageNames, $requiredDevPackageNames); 752 | } 753 | 754 | /** 755 | * @return string[] 756 | */ 757 | public function getRequirePackageNames(): array 758 | { 759 | return array_keys($this->require); 760 | } 761 | 762 | /** 763 | * @return array 764 | */ 765 | public function getProvide(): array 766 | { 767 | return $this->provide; 768 | } 769 | 770 | public function isProvidePackageSet(string $packageName): bool 771 | { 772 | return isset($this->provide[$packageName]); 773 | } 774 | 775 | /** 776 | * @param array $provide 777 | */ 778 | public function setProvide(array $provide): void 779 | { 780 | ksort($provide); 781 | 782 | $this->provide = $provide; 783 | } 784 | 785 | public function setProvidePackage(string $packageName, string $version): void 786 | { 787 | $this->provide[$packageName] = $version; 788 | } 789 | 790 | /** 791 | * @param ComposerJsonSection::* $valueName 792 | */ 793 | private function moveValueToBack(string $valueName): void 794 | { 795 | $key = array_search($valueName, $this->orderedKeys, true); 796 | if ($key !== false) { 797 | unset($this->orderedKeys[$key]); 798 | } 799 | 800 | $this->orderedKeys[] = $valueName; 801 | } 802 | 803 | /** 804 | * 2. sort item by prescribed key order 805 | * 806 | * @see https://www.designcise.com/web/tutorial/how-to-sort-an-array-by-keys-based-on-order-in-a-secondary-array-in-php 807 | * @param array $contentItems 808 | * @param string[] $orderedVisibleItems 809 | * @return mixed[] 810 | */ 811 | private function sortItemsByOrderedListOfKeys(array $contentItems, array $orderedVisibleItems): array 812 | { 813 | uksort($contentItems, function ($firstContentItem, $secondContentItem) use ($orderedVisibleItems): int { 814 | $firstItemPosition = $this->findPosition($firstContentItem, $orderedVisibleItems); 815 | $secondItemPosition = $this->findPosition($secondContentItem, $orderedVisibleItems); 816 | 817 | if ($firstItemPosition === false) { 818 | // new item, put in the back 819 | return -1; 820 | } 821 | 822 | if ($secondItemPosition === false) { 823 | // new item, put in the back 824 | return -1; 825 | } 826 | 827 | return $firstItemPosition <=> $secondItemPosition; 828 | }); 829 | 830 | return $contentItems; 831 | } 832 | 833 | private function resolveExistingAutoloadDirectory(string $autoloadDirectory): string 834 | { 835 | if ($this->fileInfo === null) { 836 | throw new ShouldNotHappenException(); 837 | } 838 | 839 | $filePathCandidates = [ 840 | $this->fileInfo->getPath() . DIRECTORY_SEPARATOR . $autoloadDirectory, 841 | // mostly tests 842 | getcwd() . DIRECTORY_SEPARATOR . $autoloadDirectory, 843 | ]; 844 | 845 | foreach ($filePathCandidates as $filePathCandidate) { 846 | if (file_exists($filePathCandidate)) { 847 | return $filePathCandidate; 848 | } 849 | } 850 | 851 | return $autoloadDirectory; 852 | } 853 | 854 | /** 855 | * @param array $packages 856 | * @return array 857 | */ 858 | private function sortPackagesIfNeeded(array $packages): array 859 | { 860 | $sortPackages = $this->config['sort-packages'] ?? false; 861 | if ($sortPackages) { 862 | return $this->composerPackageSorter->sortPackages($packages); 863 | } 864 | 865 | return $packages; 866 | } 867 | 868 | /** 869 | * @param string[] $items 870 | */ 871 | private function findPosition(string $key, array $items): int | string | bool 872 | { 873 | return array_search($key, $items, true); 874 | } 875 | } 876 | -------------------------------------------------------------------------------- /src/ValueObject/ComposerJsonManipulatorConfig.php: -------------------------------------------------------------------------------- 1 |