├── .gitignore ├── README.md ├── composer.json └── src ├── Command └── GenerateComposerReplaceAll.php ├── Composer ├── Command │ ├── ReplaceAddCommand.php │ ├── ReplaceBuildCommand.php │ ├── ReplaceBulkAddCommand.php │ ├── ReplaceBulkListCommand.php │ ├── ReplaceBulkRemoveCommand.php │ ├── ReplaceConfigAddCommand.php │ ├── ReplaceExcludeCommand.php │ ├── ReplaceIncludeCommand.php │ ├── ReplaceListCommand.php │ ├── ReplaceMultipleAddCommand.php │ ├── ReplaceRemoveCommand.php │ └── ReplaceValidateCommand.php ├── CommandProvider.php ├── Exception │ ├── EmptyBulkException.php │ ├── HttpClientException.php │ └── PackageException.php ├── Model │ ├── BulkReplacement.php │ ├── Replacement.php │ └── ReplacementCollection.php ├── Plugin.php └── Service │ └── ReplaceBuilder.php └── Test └── Unit ├── Composer ├── CommandProviderTest.php ├── Model │ └── ReplacementCollectionTest.php ├── PluginTest.php └── Service │ └── ReplaceBuilderTest.php └── Fixture └── ComposerJsonFixture.php /.gitignore: -------------------------------------------------------------------------------- 1 | .env.php 2 | vendor/ 3 | composer.lock 4 | .idea/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magento Composer Replacement Tool 2 | **This repository offers a composer plugin to help you manage composer replacements in your root `composer.json`. Once this package is installed, the composer plugin is installed, which allows you to manage replacements via specific commands (`composer replace:?`). To make sure replacements don't conflict, this plugin adds its own section `extra.replace` to your `composer.json` as well.** 3 | 4 | ## Quickstart 5 | ```bash 6 | composer require yireo/magento2-replace-tools --dev # Require this plugin 7 | composer replace:bulk:add yireo/magento2-replace-bundled # Add a replacement bulk package 8 | composer replace:build # Rebuild your composer.json based upon this 9 | composer update --lock # Actually update all your dependencies 10 | ``` 11 | 12 | ## Installation of this plugin 13 | ```bash 14 | composer require yireo/magento2-replace-tools --dev 15 | ``` 16 | 17 | ## General usage 18 | Through a series commands, this composer plugin aims to help you manage your `replace` section more efficiently. Instead of individually adding packages, packages are added in bulk through an additional composer section `extra.replace`: 19 | 20 | ```json 21 | { 22 | "replace": { 23 | "klarna/module-kp-graph-ql": "*", 24 | "magento/module-async-order-graph-ql": "*", 25 | "magento/module-authorizenet-graph-ql": "*", 26 | "magento/module-braintree-graph-ql": "*", 27 | "magento/module-bundle-graph-ql": "*", 28 | "magento/module-catalog-graph-ql": "*", 29 | ... 30 | "yireo/example-graph-ql" 31 | }, 32 | "extra": { 33 | "replace": { 34 | "bulk": [ 35 | "yireo/magento2-replace-graph-ql" 36 | ], 37 | "exclude": { 38 | "magento/module-graph-ql": "*" 39 | }, 40 | "include": { 41 | "yireo/example-graph-ql": "*" 42 | } 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | ## Replacing packages (any composer project) 49 | List all current composer replacements: 50 | ```bash 51 | composer replace:list 52 | ``` 53 | 54 | Replace a specific package: 55 | ```bash 56 | composer replace:add foo/bar '2.0' 57 | ``` 58 | 59 | Remove a specific replacement: 60 | ```bash 61 | composer replace:remove foo/bar 62 | ``` 63 | 64 | Remove a specific package (by using a version set to `*`): 65 | ```bash 66 | composer replace:add foo/bar 67 | ``` 68 | 69 | Exclude a specific package from the replace section (so include the package when installing): 70 | 71 | ```bash 72 | composer replace:exclude foo/bar 73 | ``` 74 | 75 | Include a specific package from the replace section (override a package that has been added to the replace list by a bulk package): 76 | 77 | ```bash 78 | composer replace:include foo/bar 79 | ``` 80 | 81 | ## Replacing packages by bulk (Magento-specific) 82 | Replace all Magento Multi Source Inventory packages: 83 | ```bash 84 | composer replace:bulk:add yireo/magento2-replace-inventory 85 | ``` 86 | 87 | This adds all replacements from this meta-package `yireo/magento2-replace-inventory` to your own `replace` section. And it also adds an additional section like the following: 88 | ```json 89 | "extra": { 90 | "replace": { 91 | "bulk": [ 92 | "yireo/magento2-replace-inventory" 93 | ] 94 | } 95 | }, 96 | ``` 97 | 98 | Replace all Magento GraphQL packages, but not the `magento/module-graph-ql` package itself, but again also replacing a package `yireo/example-graph-ql`: 99 | ```bash 100 | composer replace:bulk:add yireo/magento2-replace-graphql 101 | composer replace:exclude magento/module-graph-ql 102 | composer replace:include yireo/example-graph-ql 103 | composer replace:validate 104 | composer replace:build 105 | ``` 106 | 107 | This adds all replacements from this meta-package `yireo/magento2-replace-graphql` (except the package `magento/module-graph-ql` but including the package `yireo/example-graph-ql`) to your own `replace` section. And it also adds an additional section like the following: 108 | ```json 109 | { 110 | "replace": { 111 | "klarna/module-kp-graph-ql": "*", 112 | "magento/module-async-order-graph-ql": "*", 113 | "magento/module-authorizenet-graph-ql": "*", 114 | "magento/module-braintree-graph-ql": "*", 115 | "magento/module-bundle-graph-ql": "*", 116 | "magento/module-catalog-graph-ql": "*", 117 | ... 118 | "yireo/example-graph-ql" 119 | }, 120 | "extra": { 121 | "replace": { 122 | "bulk": [ 123 | "yireo/magento2-replace-graph-ql" 124 | ], 125 | "exclude": { 126 | "magento/module-graph-ql": "*" 127 | }, 128 | "include": { 129 | "yireo/example-graph-ql": "*" 130 | } 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | ### Note about `replace:build` 137 | 138 | ⚠️ Warning: Due to the nature of its implementation, `replace:build` will replace your existing `replace` section in `composer.json`. If you replaced any individual dependencies here, make sure to re-add them after `replace:build`. At the moment this composer extension does not maintain the existing `replace` section of your `composer.json`. If you want to have this tool to manage this individual dependency for you, use the `extra.replace.include` section (see above). 139 | 140 | ## Available bulk packages 141 | 142 | - [yireo/magento2-replace-bundled](https://github.com/yireo/magento2-replace-bundled) removes third party bundled extensions 143 | - [yireo/magento2-replace-content-staging](https://github.com/yireo/magento2-replace-content-staging) removes optional Content Staging modules 144 | - [yireo/magento2-replace-core](https://github.com/yireo/magento2-replace-core) removes optional core modules 145 | - [yireo/magento2-replace-graphql](https://github.com/yireo/magento2-replace-graphql) removes optional GraphQL modules 146 | - [yireo/magento2-replace-inventory](https://github.com/yireo/magento2-replace-inventory) removes optional MSI modules 147 | - [yireo/magento2-replace-sample-data](https://github.com/yireo/magento2-replace-sample-data) removes sample data modules 148 | - [yireo/magento2-replace-all](https://github.com/yireo/magento2-replace-all) removes all packages listed in the other directories 149 | 150 | Please note that the `replace` feature of composer as being used in these repositories is not well documented and probably abused a bit. If you are not willing to invest time to troubleshoot this yourself, please forget about this approach entirely so that we don't waste anyones time. 151 | 152 | ### Building composer replacements 153 | Use the following command to configure your `composer.json` for using bulk replacements: 154 | 155 | composer replace:bulk:add yireo/magento2-replace-bundled 156 | composer replace:bulk:add yireo/magento2-replace-inventory 157 | composer replace:bulk:add yireo/magento2-replace-graphql 158 | composer replace:bulk:add yireo/magento2-replace-sample-data 159 | composer replace:validate 160 | composer replace:build 161 | 162 | ### Using composer replacements 163 | Once you have a `replace` section in your composer.json file 164 | 165 | rm -r vendor/ 166 | composer update --lock 167 | 168 | Do not just use `composer install`. Do not use regular composer commands, but please follow this procedure literally and to the point. 169 | 170 | ## After having replaced Magento composer packages 171 | After you have installed a composer replacement, make sure to wipe out the `generated/` folder first and next, run `bin/magento setup:di:compile` and `bin/magento setup:upgrade` to see if Magento still works. Please note that these steps are generic developer steps, not related to this repository. 172 | 173 | rm -r generated/ 174 | bin/magento setup:di:compile 175 | bin/magento setup:upgrade 176 | 177 | ## Sample for Hyva Themes 178 | The following shows an example configuration section for your `composer.json`: 179 | ```json 180 | { 181 | "extra": { 182 | "replace": { 183 | "bulk": [ 184 | "yireo/magento2-replace-core", 185 | "yireo/magento2-replace-content-staging", 186 | "yireo/magento2-replace-inventory", 187 | "yireo/magento2-replace-bundled", 188 | "yireo/magento2-replace-graphql", 189 | "yireo/magento2-replace-sample-data" 190 | ], 191 | "exclude": { 192 | "magento/module-graph-ql": "*", 193 | "magento/module-graph-ql-cache": "*", 194 | "magento/module-catalog-graph-ql": "*", 195 | "magento/module-customer-graph-ql": "*", 196 | "magento/module-eav-graph-ql": "*", 197 | "magento/module-sales-graph-ql": "*", 198 | "magento/module-quote-graph-ql": "*" 199 | }, 200 | "include": { 201 | "magento/module-admin-graph-ql-server": "*", 202 | "magento/module-graph-ql-server": "*", 203 | "magento/page-builder": "*", 204 | "magento/module-service-proxy": "*", 205 | "magento/services-connector": "*", 206 | "magento/services-id": "*", 207 | "magento/module-services-id-graph-ql-server": "*", 208 | "magento/module-services-id-layout": "*", 209 | "magento/payment-services": "*" 210 | } 211 | } 212 | } 213 | } 214 | ``` 215 | 216 | ## FAQ 217 | #### I try to install this with `composer require a/b` but get errors 218 | Please note that this kind of question is not going to be answered anymore, except here: Do **not** use a simple `composer require a/b` command. It is not documented above, it is not part of the procedure and it does not work. Do not reason that if you know composer, you know that a simple `composer require a/b` must work. If you think composer replacements are installed the way as composer packages, you do not know composer replacements. 219 | 220 | If you want to receive support, follow along with **all** of the commands outlined above. And stick to it. Don't argue, don't reason, but stick with it. Next, if all of the workarounds with composer commands fail, only then report an issue on GitHub. 221 | 222 | #### Your extension does not work 223 | You are damn right it does not! The reason is that it is not an extension. This is **not** about installing Magento modules. This is about replacing composer packages with nothing. The *extension* is not there, it is not a Magento module. It is rather a carefully crafted composer configuration that could be copied manually or installed with the right procedure. It is a composer meta-package with an undocumented trick. If you don't like it, don't use it. 224 | 225 | #### Installing a package leads to many errors 226 | Intruiging, isn't it? Yes, this could happen. Perhaps some modules that you are replacing are in use with your own custom code. Or perhaps you are relying on other third party extensions that have yet an undocumented dependency that conflicts with this `replace` trick. If you are not willing to troubleshoot this, simply skip this trick and move on. If you are willing to troubleshoot this, copy the `replace` lines to your own `composer.json` and remove lines one-by-one until you have found the culprit. 227 | 228 | #### Is a certain package compatible with Magento 2.x.y? 229 | Theoretically, yes. Make sure to understand that these packages are not modules, not libraries, not Magento extensions. It is a gathering of 230 | hacks. So, if you understand the benefit of the `replace` trick in composer, you can use these repository to ease the pain of upgrading. 231 | 232 | One conceptual idea in these repositories is to try to keep track of the main Magento version by creating a branch `2.x.y` with a corresponding release `x.y.z`. So, Magento 2.3.5 matches with the replace branch `3.5.*`. Magento 2.4.1 matches with the replace branch `4.1`. By adding a dependency with `^4.0` in your `composer.json`, this will automatically upgrade to any `4.X` version, but exclude a major bump to `5.X`. 233 | 234 | Sometimes the actual work falls behind, which by no means indicates that the current bundling of tricks no longer works. Simply, install this package using `composer` and see if this works for you (see below). 235 | 236 | #### How do I upgrade the replacements to Magento 2.4.X? 237 | Please note the above on the versioning strategy. Once that's understood, the actual implementation is simple: `composer require yireo/magento2-replace-core:^4.0 --no-update`. 238 | 239 | #### How to test if this is working? 240 | Take your test environment. Install the relevant packages. If this works, run `bin/magento setup:di:compile` (in both Developer Mode and Production Mode) to see if there are any errors. If this fails, feel free to report an issue here. If this works, you could assume that this works ok. 241 | 242 | Remember this repository offers a smart hack, not a supported solution. You can also live with a slower Magento installation that fully complies with the Magento standards (and ships with modules you don't use and/or like). 243 | 244 | #### How do I know if something is replaced? 245 | Unfortunately, composer does not offer a CLI for this and because the replacements are stored in these packages, they are not mentioned in your own projects `composer.json` (unless you put them there). However, by opening up the `composer.lock` file and searching for the keyword `replace` you can see which packages are replaced by all packages in your installation. A simple `composer show yireo/magento2-replace-bundled` shows which replacements are included in a specific package. 246 | 247 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yireo/magento2-replace-tools", 3 | "description": "Tools to maintain yireo/magento2-replace packages", 4 | "license": "OSL-3.0", 5 | "type": "composer-plugin", 6 | "autoload": { 7 | "psr-4": { 8 | "Yireo\\ReplaceTools\\": "src/" 9 | } 10 | }, 11 | "require": { 12 | "composer-plugin-api": "^2.0", 13 | "guzzlehttp/guzzle": "^6.0 || ^7.0", 14 | "league/flysystem": "^2.0 || ^3.0", 15 | "league/flysystem-memory": "^2.0 || ^3.0", 16 | "symfony/console": "^5.1.0 || ^6.0 || ^7.0", 17 | "ext-json": "*" 18 | }, 19 | "require-dev": { 20 | "composer/composer": "^2.0", 21 | "phpunit/phpunit": "^9.2 || ^10.0" 22 | }, 23 | "extra": { 24 | "class": "Yireo\\ReplaceTools\\Composer\\Plugin" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Command/GenerateComposerReplaceAll.php: -------------------------------------------------------------------------------- 1 | getName() === 'magento2-replace-all') { 42 | continue; 43 | } 44 | 45 | echo 'Reading "'.$repository->getName().'" with branch "'.$branch.'"'."\n"; 46 | $composerFile = $repository->getRemoteComposerFile($branch); 47 | $repositoryReplacements = $composerFile->getReplace(); 48 | 49 | if (empty($repositoryReplacements)) { 50 | throw new Exception('No replacements found'); 51 | } 52 | 53 | $replacements = array_merge($replacements, $repositoryReplacements); 54 | } 55 | 56 | ksort($replacements); 57 | 58 | $composerFile = $parentRepository->getLocalComposerFile($branch); 59 | $composerFile->setReplace($replacements); 60 | $parentRepository->saveComposerFile($composerFile, $branch); 61 | } 62 | 63 | return Command::SUCCESS; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceAddCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:add'); 18 | $this->setDescription('Replace a composer package with a specific version'); 19 | $this->addArgument('package', InputArgument::REQUIRED, 'Package to replace'); 20 | $this->addArgument('version', InputArgument::OPTIONAL, 'Package version', '*'); 21 | } 22 | 23 | protected function execute(InputInterface $input, OutputInterface $output): int 24 | { 25 | $package = (string) $input->getArgument('package'); 26 | $version = (string) $input->getArgument('version'); 27 | $replaceBuilder = new ReplaceBuilder(); 28 | $replaceBuilder->replace($package, $version); 29 | $replaceBuilder->addInclude(new Replacement($package)); 30 | 31 | $output->writeln('Added composer replacement'); 32 | 33 | return Command::SUCCESS; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceBuildCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:build'); 18 | $this->setDescription('Build composer replacements based on "extra.replace"'); 19 | $this->addOption('copyExisting', 'c', InputOption::VALUE_NONE, 'Inspect your current "replace" section and try to copy it into "extra.replace"'); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output): int 23 | { 24 | $copyExisting = $input->getOption('copyExisting') !== false; 25 | $replaceBuilder = new ReplaceBuilder(); 26 | $messages = $replaceBuilder->build($copyExisting); 27 | foreach ($messages as $message) { 28 | $output->writeln($message); 29 | } 30 | 31 | $output->writeln('Your "composer.json" file has been updated. Remove the "composer.lock" file and "vendor/" folder and run "composer install" to rebuild your composer dependencies'); 32 | return Command::SUCCESS; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceBulkAddCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:bulk:add'); 18 | $this->setDescription('Add multiple replacements via a bulk package'); 19 | $this->addArgument('package', InputArgument::REQUIRED, 'Bulk package to use'); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output): int 23 | { 24 | $composerName = (string)$input->getArgument('package'); 25 | $bulkReplacement = new BulkReplacement($composerName); 26 | $replacementsFromBulk = $bulkReplacement->fetch(); 27 | if ($replacementsFromBulk->empty()) { 28 | $output->writeln('No replacements loaded from bulk'); 29 | 30 | return Command::FAILURE; 31 | } 32 | 33 | $replaceBuilder = new ReplaceBuilder(); 34 | $replaceBuilder->addBulk($bulkReplacement); 35 | 36 | foreach ($replaceBuilder->getErrors() as $error) { 37 | $output->writeln(''.$error.''); 38 | } 39 | $output->writeln('Do not forget to run "composer replace:build" afterwards'); 40 | 41 | return Command::SUCCESS; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceBulkListCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:bulk:list'); 17 | $this->setDescription('List current bulk replacements'); 18 | } 19 | 20 | protected function execute(InputInterface $input, OutputInterface $output): int 21 | { 22 | $table = new Table($output); 23 | $table->setHeaders(['Package']); 24 | 25 | $replaceBuilder = new ReplaceBuilder(); 26 | foreach ($replaceBuilder->readBulks() as $bulkReplacement) { 27 | $table->addRow([$bulkReplacement->getComposerName()]); 28 | } 29 | 30 | $table->render(); 31 | 32 | return Command::SUCCESS; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceBulkRemoveCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:bulk:remove'); 18 | $this->setDescription('Remove bulk replacement package'); 19 | $this->addArgument('package', InputArgument::REQUIRED, 'Bulk package to remove'); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output): int 23 | { 24 | $bulkPackageName = (string) $input->getArgument('package'); 25 | $bulkReplacement = new BulkReplacement($bulkPackageName); 26 | 27 | $replaceBuilder = new ReplaceBuilder(); 28 | $replaceBuilder->removeBulk($bulkReplacement); 29 | 30 | return Command::SUCCESS; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceConfigAddCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:config:add'); 17 | $this->setDescription('Add a replace configuration setting to your composer.json'); 18 | } 19 | 20 | protected function execute(InputInterface $input, OutputInterface $output): int 21 | { 22 | $replaceBuilder = new ReplaceBuilder(); 23 | $jsonData = $replaceBuilder->readJsonData(); 24 | if (array_key_exists('extra', $jsonData) && array_key_exists('replace', $jsonData['extra'])) { 25 | $output->writeln('Section "extra.replace" already exists'); 26 | 27 | return Command::SUCCESS; 28 | } 29 | 30 | $jsonData['extra']['replace'] = [ 31 | 'bulk' => [], 32 | 'include' => [], 33 | 'exclude' => [], 34 | ]; 35 | $replaceBuilder->writeJsonData($jsonData); 36 | $output->writeln('Added section "extra.replace"'); 37 | 38 | return Command::SUCCESS; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceExcludeCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:exclude'); 18 | $this->setDescription('Add a replacement to the extra.replace.exclude section'); 19 | $this->addArgument('package', InputArgument::REQUIRED, 'Composer package name'); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output): int 23 | { 24 | $package = (string) $input->getArgument('package'); 25 | $replaceBuilder = new ReplaceBuilder(); 26 | $replaceBuilder->addExclude(new Replacement($package)); 27 | 28 | $output->writeln('Excluded composer replacement'); 29 | 30 | return Command::SUCCESS; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceIncludeCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:include'); 18 | $this->setDescription('Add a replacement to the extra.replace.include section'); 19 | $this->addArgument('package', InputArgument::REQUIRED, 'Composer package name'); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output): int 23 | { 24 | $package = (string) $input->getArgument('package'); 25 | $replaceBuilder = new ReplaceBuilder(); 26 | $replaceBuilder->addInclude(new Replacement($package)); 27 | 28 | $output->writeln('Included composer replacement'); 29 | 30 | return Command::SUCCESS; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceListCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:list'); 17 | $this->setDescription('List current composer replacements'); 18 | } 19 | 20 | protected function execute(InputInterface $input, OutputInterface $output): int 21 | { 22 | $table = new Table($output); 23 | $table->setHeaders(['Package', 'Version']); 24 | 25 | $replaceBuilder = new ReplaceBuilder(); 26 | foreach ($replaceBuilder->read()->get() as $replacement) { 27 | $table->addRow([$replacement->getComposerName(), $replacement->getVersion()]); 28 | } 29 | 30 | $table->render(); 31 | 32 | return Command::SUCCESS; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceMultipleAddCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:multiple:add'); 18 | $this->setDescription('Replace multiple composer packages'); 19 | $this->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages to replace (separate multiple packages with a space)'); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output): int 23 | { 24 | $packages = $input->getArgument('packages'); 25 | 26 | foreach ($packages as $package) { 27 | $replaceBuilder = new ReplaceBuilder(); 28 | $replaceBuilder->replace($package, '*'); 29 | $replaceBuilder->addInclude(new Replacement($package)); 30 | 31 | $output->writeln('Added composer replacement for ' . $package); 32 | } 33 | 34 | return Command::SUCCESS; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceRemoveCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:remove'); 18 | $this->setDescription('Remove a composer replacement from the replace-section'); 19 | $this->addArgument('package', InputArgument::REQUIRED, 'Package to remove'); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output): int 23 | { 24 | $package = (string) $input->getArgument('package'); 25 | $replaceBuilder = new ReplaceBuilder(); 26 | $replaceBuilder->remove(new Replacement($package)); 27 | 28 | $output->writeln('Removed composer replacement'); 29 | 30 | return Command::SUCCESS; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Composer/Command/ReplaceValidateCommand.php: -------------------------------------------------------------------------------- 1 | setName('replace:validate'); 17 | $this->setDescription('Validate current composer replacements'); 18 | } 19 | 20 | protected function execute(InputInterface $input, OutputInterface $output): int 21 | { 22 | $replaceBuilder = new ReplaceBuilder(); 23 | $errors = $replaceBuilder->getErrors(); 24 | if (empty($errors)) { 25 | return Command::SUCCESS; 26 | } 27 | 28 | $table = new Table($output); 29 | $table->setHeaders(['Error']); 30 | 31 | foreach ($errors as $error) { 32 | $table->addRow([$error]); 33 | } 34 | 35 | $table->render(); 36 | 37 | return Command::FAILURE; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Composer/CommandProvider.php: -------------------------------------------------------------------------------- 1 | composerName = $composerName; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getComposerName(): string 40 | { 41 | return $this->composerName; 42 | } 43 | 44 | /** 45 | * @return int 46 | * @throws GuzzleException 47 | * @throws HttpClientException 48 | * @throws PackageException 49 | */ 50 | public function count(): int 51 | { 52 | return $this->fetch()->count(); 53 | } 54 | 55 | /** 56 | * @param Replacement $search 57 | * @return bool 58 | * @throws GuzzleException 59 | * @throws HttpClientException 60 | * @throws PackageException 61 | */ 62 | public function contains(Replacement $search): bool 63 | { 64 | foreach ($this->fetch()->get() as $replacement) { 65 | if ($replacement->getComposerName() === $search->getComposerName()) { 66 | return true; 67 | } 68 | } 69 | 70 | return false; 71 | } 72 | 73 | /** 74 | * @return ReplacementCollection 75 | * @throws HttpClientException 76 | * @throws PackageException 77 | * @throws GuzzleException 78 | */ 79 | public function fetch(): ReplacementCollection 80 | { 81 | static $collections = []; 82 | if (isset($collections[$this->getComposerName()])) { 83 | return $collections[$this->getComposerName()]; 84 | } 85 | 86 | $io = new BufferIO(); 87 | $composerConfig = Factory::createConfig($io, getcwd()); 88 | 89 | $localComposerFile = getcwd() . '/composer.json'; 90 | if (file_exists($localComposerFile)) { 91 | $localComposerConfig = json_decode(file_get_contents($localComposerFile), true); 92 | $composerConfig->merge($localComposerConfig); 93 | } 94 | 95 | $httpDownloader = Factory::createHttpDownloader($io, $composerConfig); 96 | $repositoryManager = RepositoryFactory::manager($io, $composerConfig, $httpDownloader); 97 | 98 | $composerRepositories = $composerConfig->getRepositories(); 99 | //if (empty($composerRepositories)) { 100 | //echo "No composer repositories found\n"; 101 | //} 102 | 103 | $bulk = null; 104 | foreach ($composerRepositories as $composerRepository) { 105 | //echo $composerRepository['url']."\n"; // @todo: Add debugging output 106 | try { 107 | $repository = $repositoryManager->createRepository($composerRepository['type'], $composerRepository); 108 | $bulk = $repository->findPackage($this->composerName, '*'); 109 | } catch (\Throwable $exception) { 110 | //echo "WARNING: ".$exception->getMessage()."\n"; // @todo: Add debugging output 111 | continue; 112 | } 113 | 114 | if (false === $bulk instanceof BasePackage) { 115 | continue; 116 | } 117 | 118 | echo 'Found ' . $bulk->getName() . ":" . $bulk->getVersion() . "\n"; // @todo: Add debugging output 119 | break; 120 | } 121 | 122 | if (false === $bulk instanceof BasePackage) { 123 | throw new EmptyBulkException('No bulk package found with name "' . $this->composerName . '"'); 124 | } 125 | 126 | $collection = new ReplacementCollection; 127 | foreach ($bulk->getReplaces() as $replace) { 128 | $collection->add(new Replacement($replace->getTarget(), $replace->getPrettyConstraint())); 129 | } 130 | 131 | $collections[$this->getComposerName()] = $collection; 132 | //@todo echo $io->getOutput(); 133 | 134 | return $collection; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Composer/Model/Replacement.php: -------------------------------------------------------------------------------- 1 | composerName = $composerName; 20 | $this->version = $version; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getComposerName(): string 27 | { 28 | return $this->composerName; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getVersion(): string 35 | { 36 | return $this->version; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Composer/Model/ReplacementCollection.php: -------------------------------------------------------------------------------- 1 | replacements = $replacements; 21 | } 22 | 23 | /** 24 | * @return int 25 | */ 26 | public function count(): int 27 | { 28 | return count($this->replacements); 29 | } 30 | 31 | /** 32 | * @return bool 33 | */ 34 | public function empty(): bool 35 | { 36 | return count($this->replacements) < 1; 37 | } 38 | 39 | /** 40 | * @return Replacement[] 41 | */ 42 | public function get(): array 43 | { 44 | return $this->replacements; 45 | } 46 | 47 | /** 48 | * @param Replacement $replacement 49 | * @return void 50 | */ 51 | public function add(Replacement $replacement): void 52 | { 53 | $this->replacements[] = new Replacement($this->toMagentoNs($replacement->getComposerName()), $replacement->getVersion()); 54 | 55 | if (preg_match('#^(magento|mage-os)\/#', $replacement->getComposerName())) { 56 | $this->replacements[] = new Replacement($this->toMageOSNs($replacement->getComposerName()), $replacement->getVersion()); 57 | } 58 | } 59 | 60 | public function remove(Replacement $removeReplacement) 61 | { 62 | foreach ($this->replacements as $index => $replacement) { 63 | if ($this->toMagentoNs($removeReplacement->getComposerName()) === $replacement->getComposerName()) { 64 | unset($this->replacements[$index]); 65 | } 66 | 67 | if ($this->toMageOSNs($removeReplacement->getComposerName()) === $replacement->getComposerName()) { 68 | unset($this->replacements[$index]); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * @param ReplacementCollection $replacements 75 | */ 76 | public function merge(ReplacementCollection $replacements) 77 | { 78 | foreach ($replacements->get() as $replacement) { 79 | $this->add($replacement); 80 | } 81 | } 82 | 83 | /** 84 | * @param Replacement $searchReplacement 85 | * @return bool 86 | */ 87 | public function contains(Replacement $searchReplacement): bool 88 | { 89 | foreach ($this->replacements as $replacement) { 90 | if ($replacement->getComposerName() === $searchReplacement->getComposerName()) { 91 | return true; 92 | } 93 | } 94 | 95 | return false; 96 | } 97 | 98 | /** 99 | * @return string[] 100 | */ 101 | public function toArray(): array 102 | { 103 | $replacementArray = []; 104 | foreach ($this->replacements as $replacement) { 105 | $replacementArray[$replacement->getComposerName()] = $replacement->getVersion(); 106 | } 107 | 108 | return $replacementArray; 109 | } 110 | 111 | private function toMagentoNs(string $composerName): string 112 | { 113 | return preg_replace('#^mage-os\/#', 'magento/', $composerName); 114 | } 115 | 116 | private function toMageOSNs(string $composerName): string 117 | { 118 | return preg_replace('#^magento\/#', 'mage-os/', $composerName); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Composer/Plugin.php: -------------------------------------------------------------------------------- 1 | notice('Yireo Composer Replace Tools have been activated'); 27 | } 28 | 29 | /** 30 | * @param Composer $composer 31 | * @param IOInterface $io 32 | * @return void 33 | */ 34 | public function deactivate(Composer $composer, IOInterface $io) 35 | { 36 | $io->notice('Yireo Composer Replace Tools have been deactivated'); 37 | } 38 | 39 | /** 40 | * @param Composer $composer 41 | * @param IOInterface $io 42 | * @return void 43 | */ 44 | public function uninstall(Composer $composer, IOInterface $io) 45 | { 46 | $io->notice('Yireo Composer Replace Tools have been uninstalled'); 47 | } 48 | 49 | /** 50 | * @return string[] 51 | */ 52 | public function getCapabilities() 53 | { 54 | return [ 55 | CommandProviderCapability::class => CommandProvider::class, 56 | ]; 57 | } 58 | 59 | /** 60 | * @return string[] 61 | */ 62 | public static function getSubscribedEvents() 63 | { 64 | return [ 65 | 'pre-pool-create' => 'prePoolCreate', 66 | ]; 67 | } 68 | 69 | public function prePoolCreate(PrePoolCreateEvent $event) 70 | { 71 | $newPackages = []; 72 | foreach ($event->getPackages() as $package) { 73 | /*if (false === $this->hasReplaceSource($package, '')) { 74 | $newPackages[] = $package; 75 | continue; 76 | } 77 | 78 | $package->setReplaces([]);*/ 79 | $newPackages[] = $package; 80 | } 81 | 82 | $event->setPackages($newPackages); 83 | } 84 | 85 | private function hasReplaceSource(BasePackage $package, string $replaceSource):bool 86 | { 87 | foreach ($package->getReplaces() as $replace) { 88 | if ($replace->getSource() === $replaceSource) { 89 | return true; 90 | } 91 | } 92 | 93 | return false; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Composer/Service/ReplaceBuilder.php: -------------------------------------------------------------------------------- 1 | composerFile = $composerFile; 39 | $this->filesystem = $filesystem; 40 | } 41 | 42 | /** 43 | * @return ReplacementCollection 44 | * @throws FilesystemException 45 | */ 46 | public function read(): ReplacementCollection 47 | { 48 | $jsonData = $this->readJsonData(); 49 | $replacements = $jsonData['replace'] ?? []; 50 | $collection = new ReplacementCollection(); 51 | foreach ($replacements as $package => $version) { 52 | $collection->add(new Replacement($package, $version)); 53 | } 54 | 55 | return $collection; 56 | } 57 | 58 | /** 59 | * @param ReplacementCollection $replacements 60 | * @return void 61 | * @throws FilesystemException 62 | */ 63 | public function write(ReplacementCollection $replacements) 64 | { 65 | $replacementData = []; 66 | foreach ($replacements->get() as $replacement) { 67 | $replacementData[$replacement->getComposerName()] = $replacement->getVersion(); 68 | } 69 | 70 | ksort($replacementData); 71 | 72 | $jsonData = $this->readJsonData(); 73 | $jsonData['replace'] = $replacementData; 74 | $this->writeJsonData($jsonData); 75 | } 76 | 77 | /** 78 | * @param string $package 79 | * @param string $version 80 | * @return void 81 | * @throws FilesystemException 82 | */ 83 | public function replace(string $package, string $version) 84 | { 85 | $replacements = $this->read(); 86 | $replacements->add(new Replacement($package, $version)); 87 | $this->write($replacements); 88 | } 89 | 90 | /** 91 | * @param Replacement $replacement 92 | * @return void 93 | * @throws FilesystemException 94 | */ 95 | public function remove(Replacement $replacement) 96 | { 97 | $replacements = $this->read(); 98 | $replacements->remove($replacement); 99 | $this->write($replacements); 100 | } 101 | 102 | /** 103 | * @return string[] 104 | */ 105 | public function suggestedBulks(): array 106 | { 107 | return [ 108 | 'yireo/magento2-replace-core', 109 | 'yireo/magento2-replace-bundled', 110 | 'yireo/magento2-replace-inventory', 111 | 'yireo/magento2-replace-graphql', 112 | 'yireo/magento2-replace-pagebuilder', 113 | 'yireo/magento2-replace-sample-data', 114 | 'yireo/magento2-replace-content-staging', 115 | ]; 116 | } 117 | 118 | public function isMageOS(): bool 119 | { 120 | foreach ($this->readRequires() as $require) { 121 | if ($require === 'mage-os/product-community-edition') { 122 | return true; 123 | } 124 | } 125 | 126 | return false; 127 | } 128 | 129 | /** 130 | * @return BulkReplacement[] 131 | * @throws FilesystemException 132 | */ 133 | public function readBulks(): array 134 | { 135 | $jsonData = $this->readJsonData(); 136 | if (empty($jsonData['extra']) || empty($jsonData['extra']['replace']) || empty($jsonData['extra']['replace']['bulk'])) { 137 | return []; 138 | } 139 | 140 | $bulkReplacements = []; 141 | foreach ($jsonData['extra']['replace']['bulk'] as $bulkName) { 142 | $bulkName = (string)$bulkName; 143 | if (empty($bulkName)) { 144 | continue; 145 | } 146 | 147 | $bulkReplacements[] = new BulkReplacement($bulkName); 148 | } 149 | 150 | return $bulkReplacements; 151 | } 152 | 153 | /** 154 | * @return string[] 155 | * @throws FilesystemException 156 | */ 157 | public function readRequires(): array 158 | { 159 | $jsonData = $this->readJsonData(); 160 | $requires = $jsonData['require'] ?? []; 161 | return array_keys($requires); 162 | } 163 | 164 | /** 165 | * @return ReplacementCollection 166 | * @throws FilesystemException 167 | */ 168 | public function readExcludes(): ReplacementCollection 169 | { 170 | $collection = new ReplacementCollection(); 171 | $jsonData = $this->readJsonData(); 172 | if (empty($jsonData['extra']) || empty($jsonData['extra']['replace']) || empty($jsonData['extra']['replace']['exclude'])) { 173 | return $collection; 174 | } 175 | 176 | foreach ($jsonData['extra']['replace']['exclude'] as $composerName => $version) { 177 | $collection->add(new Replacement($composerName, $version)); 178 | } 179 | 180 | return $collection; 181 | } 182 | 183 | /** 184 | * @param ReplacementCollection $collection 185 | * @return void 186 | * @throws FilesystemException 187 | */ 188 | public function writeExcludes(ReplacementCollection $collection): void 189 | { 190 | $jsonData = $this->readJsonData(); 191 | $jsonData['extra']['replace']['exclude'] = $collection->toArray(); 192 | $this->writeJsonData($jsonData); 193 | } 194 | 195 | /** 196 | * @param Replacement $replacement 197 | * @return void 198 | * @throws FilesystemException 199 | */ 200 | public function addExclude(Replacement $replacement) 201 | { 202 | $collection = $this->readExcludes(); 203 | $collection->add($replacement); 204 | $this->writeExcludes($collection); 205 | } 206 | 207 | /** 208 | * @return ReplacementCollection 209 | * @throws FilesystemException 210 | */ 211 | public function readIncludes(): ReplacementCollection 212 | { 213 | $collection = new ReplacementCollection(); 214 | $jsonData = $this->readJsonData(); 215 | if (empty($jsonData['extra']) || empty($jsonData['extra']['replace']) || empty($jsonData['extra']['replace']['include'])) { 216 | return $collection; 217 | } 218 | 219 | foreach ($jsonData['extra']['replace']['include'] as $composerName => $version) { 220 | $collection->add(new Replacement($composerName, $version)); 221 | } 222 | 223 | return $collection; 224 | } 225 | 226 | /** 227 | * @param ReplacementCollection $collection 228 | * @return void 229 | * @throws FilesystemException 230 | */ 231 | public function writeIncludes(ReplacementCollection $collection): void 232 | { 233 | $jsonData = $this->readJsonData(); 234 | $jsonData['extra']['replace']['include'] = $collection->toArray(); 235 | $this->writeJsonData($jsonData); 236 | } 237 | 238 | /** 239 | * @param Replacement $replacement 240 | * @return void 241 | * @throws FilesystemException 242 | */ 243 | public function addInclude(Replacement $replacement) 244 | { 245 | $collection = $this->readIncludes(); 246 | $collection->add($replacement); 247 | $this->writeIncludes($collection); 248 | } 249 | 250 | /** 251 | * @param BulkReplacement[] $bulkReplacements 252 | * @return void 253 | * @throws FilesystemException 254 | */ 255 | public function writeBulks(array $bulkReplacements) 256 | { 257 | $bulkReplacementArray = []; 258 | foreach ($bulkReplacements as $bulkReplacement) { 259 | $bulkReplacementArray[] = $bulkReplacement->getComposerName(); 260 | } 261 | 262 | $bulkReplacementArray = array_unique($bulkReplacementArray); 263 | $jsonData = $this->readJsonData(); 264 | $jsonData['extra']['replace']['bulk'] = $bulkReplacementArray; 265 | $this->writeJsonData($jsonData); 266 | } 267 | 268 | /** 269 | * @param BulkReplacement $bulkReplacement 270 | * @return void 271 | * @throws FilesystemException 272 | */ 273 | public function addBulk(BulkReplacement $bulkReplacement) 274 | { 275 | $bulkReplacements = $this->readBulks(); 276 | $bulkReplacements[] = $bulkReplacement; 277 | $this->writeBulks($bulkReplacements); 278 | } 279 | 280 | /** 281 | * @param BulkReplacement $bulkReplacement 282 | * @return void 283 | * @throws FilesystemException 284 | */ 285 | public function removeBulk(BulkReplacement $bulkReplacement) 286 | { 287 | $bulks = $this->readBulks(); 288 | $key = array_search($bulkReplacement->getComposerName(), $bulks); 289 | if (false !== $key) { 290 | unset($bulks[$key]); 291 | } 292 | 293 | $this->writeBulks($bulks); 294 | } 295 | 296 | /** 297 | * @return ReplacementCollection 298 | * @throws FilesystemException 299 | */ 300 | public function getConfigured(): ReplacementCollection 301 | { 302 | $collection = new ReplacementCollection(); 303 | foreach ($this->readBulks() as $bulkReplacement) { 304 | $collection->merge($bulkReplacement->fetch()); 305 | } 306 | 307 | foreach ($this->readExcludes()->get() as $excludeReplacement) { 308 | $collection->remove($excludeReplacement); 309 | } 310 | 311 | foreach ($this->readIncludes()->get() as $includeReplacement) { 312 | $collection->add($includeReplacement); 313 | } 314 | 315 | return $collection; 316 | } 317 | 318 | /** 319 | * @return string[] 320 | * @throws GuzzleException 321 | * @throws HttpClientException 322 | * @throws PackageException 323 | */ 324 | public function getErrors(): array 325 | { 326 | $errors = []; 327 | $matchingBulks = []; 328 | $configured = $this->getConfigured(); 329 | $currentReplacements = $this->read(); 330 | foreach ($currentReplacements->get() as $current) { 331 | if ($configured->contains($current)) { 332 | continue; 333 | } 334 | 335 | foreach ($this->suggestedBulks() as $bulkName) { 336 | $bulk = new BulkReplacement($bulkName); 337 | if ($bulk->contains($current)) { 338 | if (isset($matchingBulks[$bulk->getComposerName()])) { 339 | $matchingBulks[$bulk->getComposerName()][] = $current->getComposerName(); 340 | } else { 341 | $matchingBulks[$bulk->getComposerName()] = [$current->getComposerName()]; 342 | } 343 | 344 | break; 345 | } 346 | } 347 | 348 | $currentName = $current->getComposerName(); 349 | $errors[] = '"'.$currentName.'" not configured via "extra.replace"'; 350 | } 351 | 352 | foreach ($matchingBulks as $matchingBulk => $matchingBulkPackages) { 353 | $bulk = new BulkReplacement($matchingBulk); 354 | $error = 'Bulk "'.$matchingBulk.'" ('.$bulk->count().') matches with '.count($matchingBulkPackages).' entries: '; 355 | $error .= implode(', ', array_splice($matchingBulkPackages, 0, 3)); 356 | if (count($matchingBulkPackages) > 3) { 357 | $error .= ', ...'; 358 | } 359 | 360 | $errors[] = $error; 361 | } 362 | 363 | $requires = $this->readRequires(); 364 | foreach ($this->suggestedBulks() as $bulkName) { 365 | if (in_array($bulkName, $requires)) { 366 | $errors[] = 'Bulk "'.$bulkName.'" should not be in "require" section but in "extra.replace.bulk"'; 367 | } 368 | } 369 | 370 | return array_merge($errors, $this->getErrorsFromBasicStructure()); 371 | } 372 | 373 | private function getErrorsFromBasicStructure(): array 374 | { 375 | $errors = []; 376 | 377 | $jsonData = $this->readJsonData(); 378 | if (!isset($jsonData['extra'])) { 379 | return ['No section "extra" yet']; 380 | } 381 | 382 | if (!isset($jsonData['extra']['replace'])) { 383 | return ['No section "extra.replace" yet']; 384 | } 385 | 386 | $elements = array_keys($jsonData['extra']['replace']); 387 | $allowedElements = ['bulk', 'include', 'exclude']; 388 | foreach ($elements as $element) { 389 | if (!in_array($element, $allowedElements)) { 390 | $errors[] = 'Unknown element "'.$element.'" in section "extra.replace"'; 391 | } 392 | } 393 | 394 | return $errors; 395 | } 396 | 397 | /** 398 | * @param bool $copyExisting 399 | * @return string[] 400 | * @throws FilesystemException 401 | */ 402 | public function build(bool $copyExisting = false): array 403 | { 404 | $messages = []; 405 | $configuredReplacements = $this->getConfigured(); 406 | 407 | if ($copyExisting === true) { 408 | $currentReplacements = $this->read(); 409 | foreach ($currentReplacements->get() as $currentReplacement) { 410 | if (false === $configuredReplacements->contains($currentReplacement)) { 411 | $this->addInclude($currentReplacement); 412 | $configuredReplacements->add($currentReplacement); 413 | $messages[] = 'Adding replacement "'.$currentReplacement->getComposerName( 414 | ).'" to "extra.replace.include"'; 415 | } 416 | } 417 | 418 | } 419 | 420 | $this->write($configuredReplacements); 421 | 422 | return $messages; 423 | } 424 | 425 | /** 426 | * @return array 427 | * @throws FilesystemException 428 | */ 429 | public function readJsonData(): array 430 | { 431 | return json_decode($this->filesystem->read($this->composerFile), true); 432 | } 433 | 434 | /** 435 | * @param array $jsonData 436 | * @return void 437 | * @throws FilesystemException 438 | */ 439 | public function writeJsonData(array $jsonData) 440 | { 441 | $contents = json_encode($jsonData, JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES); 442 | $this->filesystem->write($this->composerFile, $contents); 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /src/Test/Unit/Composer/CommandProviderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(CommandProvider::class, $commandProvider); 16 | 17 | $commands = $commandProvider->getCommands(); 18 | foreach ($commands as $command) { 19 | $this->assertInstanceOf(Command::class, $command); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Test/Unit/Composer/Model/ReplacementCollectionTest.php: -------------------------------------------------------------------------------- 1 | add(new Replacement('foo/bar')); 15 | $this->assertTrue(count($replacementCollection->get()) === 1); 16 | $first = $replacementCollection->get()[0]; 17 | $this->assertEquals('foo/bar', $first->getComposerName()); 18 | } 19 | 20 | public function testMagentoPackageAdd() 21 | { 22 | $replacementCollection = new ReplacementCollection(); 23 | $replacementCollection->add(new Replacement('magento/bar')); 24 | $this->assertTrue(count($replacementCollection->get()) === 2); 25 | 26 | $foundMagento = false; 27 | $foundMageOS = false; 28 | foreach ($replacementCollection->get() as $replacement) { 29 | if ($replacement->getComposerName() === 'magento/bar') { 30 | $foundMagento = true; 31 | } 32 | 33 | if ($replacement->getComposerName() === 'mage-os/bar') { 34 | $foundMageOS = true; 35 | } 36 | } 37 | 38 | $this->assertTrue($foundMagento); 39 | $this->assertTrue($foundMageOS); 40 | } 41 | 42 | public function testMageOSPackageAdd() 43 | { 44 | $replacementCollection = new ReplacementCollection(); 45 | $replacementCollection->add(new Replacement('mage-os/bar')); 46 | $this->assertTrue(count($replacementCollection->get()) === 2); 47 | 48 | $foundMagento = false; 49 | $foundMageOS = false; 50 | foreach ($replacementCollection->get() as $replacement) { 51 | if ($replacement->getComposerName() === 'magento/bar') { 52 | $foundMagento = true; 53 | } 54 | 55 | if ($replacement->getComposerName() === 'mage-os/bar') { 56 | $foundMageOS = true; 57 | } 58 | } 59 | 60 | $this->assertTrue($foundMagento); 61 | $this->assertTrue($foundMageOS); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Test/Unit/Composer/PluginTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Plugin::class, $plugin); 14 | 15 | $capabilities = $plugin->getCapabilities(); 16 | $this->assertNotEmpty($capabilities); 17 | $this->assertTrue(count($capabilities) === 1); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Test/Unit/Composer/Service/ReplaceBuilderTest.php: -------------------------------------------------------------------------------- 1 | getFixture() 18 | ->addReplace('yireo/foobar1') 19 | ->addReplace('yireo/foobar2') 20 | ->output(); 21 | $filesystem = $this->getFilesystemFixture($composerJson); 22 | $replaceBuilder = new ReplaceBuilder($filesystem); 23 | 24 | $collection = $replaceBuilder->read(); 25 | $this->assertEquals(2, $collection->count()); 26 | 27 | $collection->add(new Replacement('yireo/foobar3')); 28 | $replaceBuilder->write($collection); 29 | 30 | $collection = $replaceBuilder->read(); 31 | $this->assertEquals(3, $collection->count()); 32 | 33 | $collection->add(new Replacement('yireo/foobar3')); 34 | $replaceBuilder->write($collection); 35 | 36 | $collection = $replaceBuilder->read(); 37 | $this->assertEquals(3, $collection->count()); 38 | } 39 | 40 | public function testValidateIncludeReplacement() 41 | { 42 | $composerJson = $this->getFixture() 43 | ->addIncludeReplace('yireo/foobar1') 44 | ->addReplace('yireo/foobar2') 45 | ->output(); 46 | $filesystem = $this->getFilesystemFixture($composerJson); 47 | $replaceBuilder = new ReplaceBuilder($filesystem); 48 | 49 | $errors = $replaceBuilder->getErrors(); 50 | $this->assertEquals(1, count($errors)); 51 | $this->assertStringContainsString('"yireo/foobar2" not configured via "extra.replace"', $errors[0]); 52 | 53 | $replaceBuilder->build(); 54 | $errors = $replaceBuilder->getErrors(); 55 | $this->assertEquals(0, count($errors)); 56 | } 57 | 58 | public function testValidateBulkReplacements() 59 | { 60 | $composerJson = $this->getFixture() 61 | ->addBulkReplace('yireo/magento2-replace-graphql') 62 | ->output(); 63 | $filesystem = $this->getFilesystemFixture($composerJson); 64 | $replaceBuilder = new ReplaceBuilder($filesystem); 65 | 66 | $errors = $replaceBuilder->getErrors(); 67 | $this->assertEquals(0, count($errors), 'Expected no errors'); 68 | 69 | $replaceBuilder->build(); 70 | $collection = $replaceBuilder->read(); 71 | $this->assertTrue($collection->count() > 40); 72 | } 73 | 74 | public function testValidateWrongBulkReplacement() 75 | { 76 | $composerJson = $this->getFixture() 77 | ->addBulkReplace('yireo/not-existing') 78 | ->output(); 79 | $filesystem = $this->getFilesystemFixture($composerJson); 80 | $replaceBuilder = new ReplaceBuilder($filesystem); 81 | 82 | $this->expectException(ClientException::class); 83 | $replaceBuilder->build(); 84 | } 85 | 86 | public function testValidateWithEmptyBulkReplacement() 87 | { 88 | $composerJson = $this->getFixture() 89 | ->addBulkReplace('yireo/magento2-replace-tools') 90 | ->output(); 91 | $filesystem = $this->getFilesystemFixture($composerJson); 92 | $replaceBuilder = new ReplaceBuilder($filesystem); 93 | 94 | $collection = $replaceBuilder->getConfigured(); 95 | $this->assertEquals(0, $collection->count()); 96 | } 97 | 98 | private function getFixture(): ComposerJsonFixture 99 | { 100 | return new ComposerJsonFixture(); 101 | } 102 | 103 | private function getFilesystemFixture(string $composerJson) 104 | { 105 | $memoryAdapter = new InMemoryFilesystemAdapter(); 106 | $filesystem = new Filesystem($memoryAdapter); 107 | $filesystem->write('/composer.json', $composerJson); 108 | return $filesystem; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Test/Unit/Fixture/ComposerJsonFixture.php: -------------------------------------------------------------------------------- 1 | require[$packageName] = $version; 40 | return $this; 41 | } 42 | 43 | /** 44 | * @param string $packageName 45 | * @param string $version 46 | * @return $this 47 | */ 48 | 49 | public function addReplace(string $packageName, string $version = '*'): ComposerJsonFixture 50 | { 51 | $this->replace[$packageName] = $version; 52 | return $this; 53 | } 54 | 55 | /** 56 | * @param string $packageName 57 | * @return $this 58 | */ 59 | public function addBulkReplace(string $packageName): ComposerJsonFixture 60 | { 61 | $this->bulkReplace[] = $packageName; 62 | return $this; 63 | } 64 | 65 | /** 66 | * @param string $packageName 67 | * @param string $version 68 | * @return $this 69 | */ 70 | public function addIncludeReplace(string $packageName, string $version = '*'): ComposerJsonFixture 71 | { 72 | $this->includeReplace[$packageName] = $version; 73 | return $this; 74 | } 75 | 76 | /** 77 | * @param string $packageName 78 | * @param string $version 79 | * @return $this 80 | */ 81 | public function addExcludeReplace(string $packageName, string $version = '*'): ComposerJsonFixture 82 | { 83 | $this->excludeReplace[$packageName] = $version; 84 | return $this; 85 | } 86 | 87 | /** 88 | * @return string 89 | */ 90 | public function output(): string 91 | { 92 | $data = [ 93 | 'require' => $this->require, 94 | 'replace' => $this->replace, 95 | 'extra' => [ 96 | 'replace' => [ 97 | 'bulk' => $this->bulkReplace, 98 | 'include' => $this->includeReplace, 99 | 'exclude' => $this->excludeReplace, 100 | ] 101 | ] 102 | ]; 103 | 104 | return json_encode($data, JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES); 105 | } 106 | } 107 | --------------------------------------------------------------------------------