├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug-report.md └── workflows │ ├── integration.yml │ └── tests.yml ├── .gitignore ├── README.md ├── composer.json ├── phpcs.xml ├── phpstan.neon ├── phpunit.xml ├── src ├── PackageRequiresAdjuster.php └── Plugin.php └── tests └── PackageRequiresAdjusterTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.php] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.neon] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mglaman 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Something isn't working right 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Composer version** 14 | 15 | ``` 16 | Composer version … 17 | ``` 18 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | verify: 9 | runs-on: "ubuntu-latest" 10 | name: "verify" 11 | strategy: 12 | matrix: 13 | drupal: 14 | - "10.0.x-dev" 15 | - "10.0.0-alpha4" 16 | - "^10.0@alpha" 17 | steps: 18 | - name: "Checkout" 19 | uses: "actions/checkout@v2" 20 | - name: "Install PHP" 21 | uses: "shivammathur/setup-php@v2" 22 | with: 23 | coverage: none 24 | php-version: 8.1 25 | tools: composer:v2 26 | extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, bcmath, gd, exif, iconv 27 | - name: Setup Drupal 28 | uses: bluehorndigital/setup-drupal@v1.0.4 29 | with: 30 | version: "${{ matrix.drupal }}" 31 | path: ~/drupal 32 | allow_plugins: | 33 | drupal/core-composer-scaffold 34 | drupal/core-project-message 35 | mglaman/composer-drupal-lenient 36 | - name: Require self 37 | run: | 38 | cd ~/drupal 39 | composer require mglaman/composer-drupal-lenient *@dev 40 | - name: Configure allowed list 41 | run: | 42 | cd ~/drupal 43 | composer config --merge --json extra.drupal-lenient.allowed-list '["drupal/token"]' 44 | - name: Add non-compatible module release 45 | run: | 46 | cd ~/drupal 47 | composer require drupal/token:1.10.0 -W 48 | composer show drupal/token 49 | purge: 50 | runs-on: "ubuntu-latest" 51 | name: "purge#1342bb92" 52 | steps: 53 | - name: "Checkout" 54 | uses: "actions/checkout@v2" 55 | - name: "Install PHP" 56 | uses: "shivammathur/setup-php@v2" 57 | with: 58 | coverage: none 59 | php-version: 8.1 60 | tools: composer:v2 61 | extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, bcmath, gd, exif, iconv 62 | - name: Setup Drupal 63 | uses: bluehorndigital/setup-drupal@v1.0.4 64 | with: 65 | version: ^10.0@alpha 66 | path: ~/drupal 67 | allow_plugins: | 68 | mglaman/composer-drupal-lenient 69 | - name: Require self 70 | run: | 71 | cd ~/drupal 72 | composer require mglaman/composer-drupal-lenient *@dev 73 | - name: Configure allowed list 74 | run: | 75 | cd ~/drupal 76 | composer config --merge --json extra.drupal-lenient.allowed-list '["drupal/purge"]' 77 | - name: Add non-compatible module release 78 | run: | 79 | cd ~/drupal 80 | composer require drupal/purge:3.x-dev#1342bb92b5304c6c316357d2c2c71a62a34d3eff -W 81 | composer show drupal/purge 82 | d9_to_d10: 83 | runs-on: "ubuntu-latest" 84 | name: "D9 to D10" 85 | steps: 86 | - name: "Checkout" 87 | uses: "actions/checkout@v2" 88 | - name: "Install PHP" 89 | uses: "shivammathur/setup-php@v2" 90 | with: 91 | coverage: none 92 | php-version: 8.1 93 | tools: composer:v2 94 | extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, bcmath, gd, exif, iconv 95 | - name: Setup Drupal 96 | uses: bluehorndigital/setup-drupal@v1.0.4 97 | with: 98 | version: ^9 99 | path: ~/drupal 100 | allow_plugins: | 101 | mglaman/composer-drupal-lenient 102 | - name: Require self 103 | run: | 104 | cd ~/drupal 105 | composer require mglaman/composer-drupal-lenient *@dev 106 | - name: Configure allowed list 107 | run: | 108 | cd ~/drupal 109 | composer config --merge --json extra.drupal-lenient.allowed-list '["drupal/purge"]' 110 | - name: Add module release 111 | run: | 112 | cd ~/drupal 113 | composer require drupal/purge:3.x-dev#1342bb92b5304c6c316357d2c2c71a62a34d3eff -W 114 | composer show drupal/purge 115 | - name: Upgrade Drupal 116 | run: | 117 | cd ~/drupal 118 | composer require drupal/core-recommended:^10@alpha drupal/core-dev:^10@alpha drupal/core-composer-scaffold:^10@alpha drupal/core-project-message:^10@alpha drush/drush:^11.0 guzzlehttp/guzzle:^7.0 --with-all-dependencies --no-update 119 | composer update --no-progress --prefer-dist 120 | - name: Show module 121 | run: | 122 | cd ~/drupal 123 | composer show drupal/purge 124 | without_lock_file: 125 | runs-on: "ubuntu-latest" 126 | name: "without lock file" 127 | steps: 128 | - name: "Checkout" 129 | uses: "actions/checkout@v2" 130 | - name: "Install PHP" 131 | uses: "shivammathur/setup-php@v2" 132 | with: 133 | coverage: none 134 | php-version: 8.1 135 | tools: composer:v2 136 | extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, bcmath, gd, exif, iconv 137 | - name: Setup Drupal 138 | uses: bluehorndigital/setup-drupal@v1.0.4 139 | with: 140 | version: "^10" 141 | path: ~/drupal 142 | allow_plugins: | 143 | drupal/core-composer-scaffold 144 | drupal/core-project-message 145 | mglaman/composer-drupal-lenient 146 | - name: Require self 147 | run: | 148 | cd ~/drupal 149 | composer require mglaman/composer-drupal-lenient *@dev 150 | - name: Configure allowed list 151 | run: | 152 | cd ~/drupal 153 | composer config --merge --json extra.drupal-lenient.allowed-list '["drupal/token"]' 154 | - name: Remove lock file 155 | run: | 156 | cd ~/drupal 157 | rm composer.lock 158 | rm -rf vendor 159 | - name: Require self globally 160 | run: | 161 | cd ~/drupal 162 | composer global config --no-plugins allow-plugins.mglaman/composer-drupal-lenient true 163 | composer global require mglaman/composer-drupal-lenient *@dev 164 | - name: Add non-compatible module release 165 | run: | 166 | cd ~/drupal 167 | composer require drupal/token:1.10.0 -W 168 | composer show drupal/token 169 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | phpcs: 9 | runs-on: "ubuntu-latest" 10 | name: PHPCS 11 | steps: 12 | - name: "Checkout" 13 | uses: "actions/checkout@v2" 14 | - name: "Install PHP" 15 | uses: "shivammathur/setup-php@v2" 16 | with: 17 | coverage: none 18 | php-version: 8.1 19 | tools: composer:v2 20 | - name: "Install dependencies" 21 | run: "composer update --no-progress --prefer-dist" 22 | - name: "PHPCS" 23 | run: "php vendor/bin/phpcs" 24 | phpstan: 25 | runs-on: "ubuntu-latest" 26 | name: PHPStan 27 | steps: 28 | - name: "Checkout" 29 | uses: "actions/checkout@v2" 30 | - name: "Install PHP" 31 | uses: "shivammathur/setup-php@v2" 32 | with: 33 | coverage: none 34 | php-version: 8.1 35 | tools: composer:v2 36 | - name: "Install dependencies" 37 | run: "composer update --no-progress --prefer-dist" 38 | - name: "PHPStan" 39 | run: "php vendor/bin/phpstan analyze" 40 | phpunuit: 41 | runs-on: "ubuntu-latest" 42 | name: PHPUnit 43 | steps: 44 | - name: "Checkout" 45 | uses: "actions/checkout@v2" 46 | - name: "Install PHP" 47 | uses: "shivammathur/setup-php@v2" 48 | with: 49 | coverage: xdebug 50 | php-version: 8.1 51 | tools: composer:v2 52 | - name: "Install dependencies" 53 | run: "composer update --no-progress --prefer-dist" 54 | - name: "PHPUnit" 55 | run: "php vendor/bin/phpunit" 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .phpunit.cache 2 | composer.lock 3 | vendor 4 | .idea 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drupal Lenient Composer Plugin 2 | 3 | Lenient with it, Drupal 11 with it. 4 | 5 | ## Why? 6 | 7 | The Drupal community introduced a lenient Composer facade that modified the `drupal/core` constraint for packages. This 8 | was done to remove a barrier with getting extensions installed via Composer to work on making modules Drupal 9 ready. 9 | 10 | We hit the same problem, again. At DrupalCon Portland we sat down and decided a Composer plugin is the best approach. 11 | 12 | See [Add a composer plugin that supports 'composer require-lenient' to support major version transitions](https://www.drupal.org/project/drupal/issues/3267143). 13 | 14 | Drupal documentation page: [Using Drupal's Lenient Composer Endpoint](https://www.drupal.org/docs/develop/using-composer/using-drupals-lenient-composer-endpoint). 15 | 16 | ## How 17 | 18 | This subscribes to `PluginEvents::PRE_POOL_CREATE` and filters packages. This is inspired by `symfony/flex`, but it does 19 | not filter out packages. It rewrites the `drupal/core` constraint on any package with a type of `drupal-*`, 20 | excluding `drupal-core`. The constraint is set to `'^8 || ^9 || ^10 || ^11 || ^12'` for `drupal/core`. 21 | 22 | ## Try it 23 | 24 | Set up a fresh Drupal 11 site with this plugin (remember to press `y` for the new `allow-plugins` prompt.) 25 | 26 | ```shell 27 | composer create-project drupal/recommended-project d11 28 | cd d11 29 | composer require mglaman/composer-drupal-lenient 30 | ``` 31 | 32 | The plugin only works against specified packages. To allow a package to have a lenient Drupal core version constraint, 33 | you must add it to `extra.drupal-lenient.allowed-list`. The following is an example to add Simplenews via the command line 34 | with `composer config` 35 | 36 | ```shell 37 | composer config --merge --json extra.drupal-lenient.allowed-list '["drupal/simplenews"]' 38 | ``` 39 | 40 | Now, add a module that does [not have a Drupal 11 compatible](https://dev.acquia.com/drupal11/deprecation_status/projects?next_step=Fix%20deprecation%20errors%20found) release! 41 | 42 | ```shell 43 | composer require drupal/simplenews 44 | ``` 45 | 46 | 🥳 Now you can use [cweagans/composer-patches](https://github.com/cweagans/composer-patches) to patch the module for Drupal 11 compatibility! 47 | 48 | For a quick start, allow installing the module by installing [Backward Compatibility](https://www.drupal.org/project/backward_compatibility): 49 | 50 | > Backward Compatibility allows you to install old Drupal modules in current Drupal. 51 | 52 | Alternatively, manually add the latest version in the module `*.info.yml` file: 53 | 54 | ```shell 55 | core_version_requirement: ^9.3 || ^10 || ^11 56 | ``` 57 | 58 | ## Allowing all packages 59 | 60 | If you want to allow all packages to have a lenient Drupal core version constraint, you can set `extra.drupal-lenient.allow-all` to `true`. 61 | 62 | ```shell 63 | composer config --json extra.drupal-lenient.allow-all true 64 | ``` 65 | 66 | Using `allow-all` allows you to install any package without needing to add it to the `allowed-list`. 67 | 68 | ## Support when `composer.lock` removed 69 | 70 | This plugin must be installed globally if your project's `composer.lock` file is removed. 71 | 72 | ```shell 73 | composer global config --no-plugins allow-plugins.mglaman/composer-drupal-lenient true 74 | composer global require mglaman/composer-drupal-lenient 75 | ``` 76 | 77 | **Warning**: this means the plugin will run on all Composer commands. This is not recommended, but it is the only way 78 | the plugin can work when `composer.lock` is removed. 79 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mglaman/composer-drupal-lenient", 3 | "type": "composer-plugin", 4 | "license": "GPL-2.0-or-later", 5 | "autoload": { 6 | "psr-4": { 7 | "ComposerDrupalLenient\\": "src/" 8 | } 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Matt Glaman", 13 | "email": "nmd.matt@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=8.1", 18 | "composer-plugin-api": "^2.0" 19 | }, 20 | "extra": { 21 | "class": "ComposerDrupalLenient\\Plugin" 22 | }, 23 | "require-dev": { 24 | "composer/composer": "^2.3", 25 | "phpstan/extension-installer": "^1.1", 26 | "phpstan/phpstan": "^1.6", 27 | "phpstan/phpstan-phpunit": "^1.1", 28 | "phpstan/phpstan-strict-rules": "^1.2", 29 | "phpunit/phpunit": "^9.5", 30 | "squizlabs/php_codesniffer": "^3.6" 31 | }, 32 | "config": { 33 | "allow-plugins": { 34 | "phpstan/extension-installer": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | src 8 | tests 9 | 10 | 11 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 9 3 | paths: 4 | - src 5 | includes: 6 | - vendor/phpstan/phpstan/conf/bleedingEdge.neon 7 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 23 | 24 | src 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/PackageRequiresAdjuster.php: -------------------------------------------------------------------------------- 1 | drupalCoreConstraint = (new VersionParser()) 25 | ->parseConstraints('^8 || ^9 || ^10 || ^11 || ^12'); 26 | } 27 | 28 | public function applies(PackageInterface $package): bool 29 | { 30 | $type = $package->getType(); 31 | if ( 32 | $type === 'drupal-core' 33 | || (!str_starts_with($type, 'drupal-') && $type !== 'metapackage') 34 | ) { 35 | return false; 36 | } 37 | /** 38 | * @var array{drupal-lenient?: array{allow-all?: bool, allowed-list?: list|mixed}} $extra 39 | */ 40 | $extra = $this->composer->getPackage()->getExtra(); 41 | $allowAll = $extra['drupal-lenient']['allow-all'] ?? false; 42 | if ($allowAll) { 43 | return true; 44 | } 45 | $allowedList = $extra['drupal-lenient']['allowed-list'] ?? []; 46 | if (!is_array($allowedList) || count($allowedList) === 0) { 47 | return false; 48 | } 49 | return in_array($package->getName(), $allowedList, true); 50 | } 51 | 52 | public function adjust(PackageInterface $package): void 53 | { 54 | $requires = array_map(function (Link $link) { 55 | if ($link->getDescription() === Link::TYPE_REQUIRE && $link->getTarget() === 'drupal/core') { 56 | return new Link( 57 | $link->getSource(), 58 | $link->getTarget(), 59 | $this->drupalCoreConstraint, 60 | $link->getDescription(), 61 | $this->drupalCoreConstraint->getPrettyString() 62 | ); 63 | } 64 | return $link; 65 | }, $package->getRequires()); 66 | // @note `setRequires` is on Package but not PackageInterface. 67 | if ($package instanceof CompletePackage) { 68 | $package->setRequires($requires); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | getPackages(); 21 | foreach ($packages as $package) { 22 | if ($this->packageRequiresAdjuster->applies($package)) { 23 | $this->packageRequiresAdjuster->adjust($package); 24 | } 25 | } 26 | $event->setPackages($packages); 27 | } 28 | 29 | public function activate(Composer $composer, IOInterface $io): void 30 | { 31 | $this->packageRequiresAdjuster = new PackageRequiresAdjuster($composer); 32 | } 33 | 34 | public function deactivate(Composer $composer, IOInterface $io): void 35 | { 36 | } 37 | 38 | public function uninstall(Composer $composer, IOInterface $io): void 39 | { 40 | } 41 | 42 | public static function getSubscribedEvents(): array 43 | { 44 | return [ 45 | PluginEvents::PRE_POOL_CREATE => 'modifyPackages', 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/PackageRequiresAdjusterTest.php: -------------------------------------------------------------------------------- 1 | setExtra([ 30 | 'drupal-lenient' => [ 31 | 'allowed-list' => [ 32 | 'foo', 33 | ] 34 | ] 35 | ]); 36 | $composer = new Composer(); 37 | $composer->setPackage($root); 38 | 39 | $adjuster = new PackageRequiresAdjuster($composer); 40 | $package = new Package($name, '1.0', '1.0'); 41 | $package->setType($type); 42 | self::assertEquals($expected, $adjuster->applies($package)); 43 | } 44 | 45 | /** 46 | * @covers ::__construct 47 | * @covers ::applies 48 | * @dataProvider provideTypes 49 | */ 50 | public function testAppliesWithAllowsAll(string $name, string $type): void 51 | { 52 | $root = new RootPackage('foo', '1.0', '1.0'); 53 | $root->setExtra([ 54 | 'drupal-lenient' => [ 55 | 'allow-all' => true, 56 | ] 57 | ]); 58 | $composer = new Composer(); 59 | $composer->setPackage($root); 60 | 61 | $adjuster = new PackageRequiresAdjuster($composer); 62 | $package = new Package($name, '1.0', '1.0'); 63 | $package->setType($type); 64 | 65 | $expected = !(($type === 'library' || $type === 'drupal-core')); 66 | self::assertEquals( 67 | $expected, 68 | $adjuster->applies($package), 69 | "Package $name of type $type should be allowed." 70 | ); 71 | } 72 | 73 | /** 74 | * @return array> 75 | */ 76 | public function provideTypes(): array 77 | { 78 | // Taken from https://github.com/composer/installers. 79 | return [ 80 | ['foo', 'library', false], 81 | ['foo', 'drupal-core', false], 82 | ['foo', 'drupal-module', true], 83 | ['foo', 'drupal-theme', true], 84 | ['foo', 'drupal-library', true], 85 | ['foo', 'drupal-profile', true], 86 | ['foo', 'drupal-database-driver', true], 87 | ['foo', 'drupal-drush', true], 88 | ['foo', 'drupal-custom-theme', true], 89 | ['foo', 'drupal-custom-module', true], 90 | ['foo', 'drupal-custom-profile', true], 91 | ['foo', 'drupal-custom-multisite', true], 92 | ['foo', 'drupal-console', true], 93 | ['foo', 'drupal-console-language', true], 94 | ['foo', 'drupal-config', true], 95 | ['foo', 'metapackage', true], 96 | ['bar', 'drupal-module', false], 97 | ['baz', 'drupal-theme', false], 98 | ['baz', 'metapackage', false] 99 | ]; 100 | } 101 | 102 | /** 103 | * @covers ::__construct 104 | * @covers ::adjust 105 | * 106 | * @dataProvider provideAdjustData 107 | */ 108 | public function testAdjust(?string $coreVersion, string $expectedCoreConstraintString): void 109 | { 110 | $composer = new Composer(); 111 | $root = new RootPackage('foo', '1.0', '1.0'); 112 | $composer->setPackage($root); 113 | $adjuster = new PackageRequiresAdjuster($composer); 114 | $originalDrupalCoreConstraint = new MultiConstraint([ 115 | new Constraint('>=', '8.0'), 116 | new Constraint('>=', '9.0'), 117 | new Constraint('>=', '10.0'), 118 | new Constraint('>=', '11.0'), 119 | ]); 120 | $originalSimplenewsConstraint = new Constraint('>=', '4.0.0'); 121 | $package = new CompletePackage('foo', '1.0', '1.0'); 122 | $package->setType('drupal-module'); 123 | $package->setRequires([ 124 | 'drupal/core' => new Link( 125 | 'bar', 126 | 'drupal/core', 127 | $originalDrupalCoreConstraint, 128 | Link::TYPE_REQUIRE, 129 | $originalDrupalCoreConstraint->getPrettyString() 130 | ), 131 | 'drupal/simplenews' => new Link( 132 | 'bar', 133 | 'drupal/simplenews', 134 | $originalSimplenewsConstraint, 135 | Link::TYPE_REQUIRE, 136 | $originalSimplenewsConstraint->getPrettyString() 137 | ) 138 | ]); 139 | $adjuster->adjust($package); 140 | self::assertEquals( 141 | $expectedCoreConstraintString, 142 | $package->getRequires()['drupal/core']->getConstraint()->getPrettyString() 143 | ); 144 | self::assertSame( 145 | $originalSimplenewsConstraint, 146 | $package->getRequires()['drupal/simplenews']->getConstraint() 147 | ); 148 | if ($coreVersion !== null) { 149 | self::assertTrue( 150 | (new Constraint('==', $coreVersion))->matches($package->getRequires()['drupal/core']->getConstraint()) 151 | ); 152 | } 153 | } 154 | 155 | /** 156 | * @return array> 157 | */ 158 | public function provideAdjustData(): array 159 | { 160 | return [ 161 | [null, '^8 || ^9 || ^10 || ^11 || ^12'], 162 | ['10.0.0-alpha5', '^8 || ^9 || ^10 || ^11 || ^12'], 163 | ]; 164 | } 165 | } 166 | --------------------------------------------------------------------------------