├── .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 |
--------------------------------------------------------------------------------