| name | drupal-contrib-mgmt |
| description | Comprehensive guide for managing Drupal contributed modules via Composer, including updates, patches, version compatibility, and Drupal 11 upgrades. Use when updating modules or resolving dependency issues. |
Drupal Contrib Module Management
Core Update Workflow
Standard Module Update
composer require drupal/module_name --with-all-dependencies
composer require drupal/module_name:^3.0 --with-all-dependencies
composer require drupal/module_a drupal/module_b --with-all-dependencies
drush updb -y
drush cr
Major Version Upgrades
When upgrading to a new major version (e.g., 2.x → 3.x):
- Check compatibility: Ensure module supports your Drupal core version
- Search issue queue for patches:
https://www.drupal.org/project/issues/MODULE_NAME?categories=All
- Use Drupal Lenient for version requirement issues (see below)
- Apply patches via composer.json (see Patch Management section)
- Run upgrade_status to check for deprecations
Checking Drupal 11 Compatibility
Three methods to check if a module is D11 compatible (in order of preference):
Method 1: Check .info.yml File (Fastest, Most Reliable)
cat docroot/modules/contrib/MODULE_NAME/MODULE_NAME.info.yml | grep core_version_requirement
What to look for:
core_version_requirement: ^9.5 || ^10 || ^11
core_version_requirement: ^8 || ^9 || ^10 || ^11
core_version_requirement: ^9 || ^10
Example:
$ cat docroot/modules/contrib/admin_toolbar/admin_toolbar.info.yml | grep core_version
core_version_requirement: ^9.5 || ^10 || ^11
Method 2: Use Composer Commands (Works Before Installing)
composer show drupal/MODULE_NAME --all | grep -A5 "^versions"
composer show drupal/MODULE_NAME | grep versions
What to look for:
- Version number (e.g., 3.6.2)
- Check Drupal.org for release notes mentioning D11
Method 3: Check Drupal.org Project Page
Only use as fallback when above methods aren't conclusive.
https://www.drupal.org/project/MODULE_NAME
Look for:
- Latest release notes mentioning "Drupal 11"
- Module page header showing D11 compatibility badge
- Issue queue for D11 compatibility issues
Important Notes:
- ⚠️ Module may declare D11 support but still have deprecation warnings
- ⚠️ upgrade_status warnings don't mean module is incompatible
- ⚠️ "Check manually" status often means runtime version checks (false positive)
- ✅ If .info.yml declares
^11 support, module maintainer says it works
Real-World Examples:
$ cat docroot/modules/contrib/admin_toolbar/admin_toolbar.info.yml | grep core_version
core_version_requirement: ^9.5 || ^10 || ^11
$ cat docroot/modules/contrib/audiofield/audiofield.info.yml | grep core_version
core_version_requirement: ^8 || ^9 || ^10 || ^11
Drupal Lenient Plugin
The mglaman/composer-drupal-lenient plugin allows installing modules that haven't updated their version requirements yet.
Setup
{
"require": {
"mglaman/composer-drupal-lenient": "^1.0"
},
"config": {
"allow-plugins": {
"mglaman/composer-drupal-lenient": true
}
},
"extra": {
"drupal-lenient": {
"allowed-list": [
"drupal/module_name",
"drupal/another_module"
]
}
}
}
Usage
composer require drupal/module_name --with-all-dependencies
Patch Management (cweagans/composer-patches)
IMPORTANT: Use version 2.x for reliable patch application. Version 1.x uses the patch binary which can have issues on some systems. Version 2.x uses git apply by default.
Patch Configuration
{
"require": {
"cweagans/composer-patches": "^2.0"
},
"config": {
"allow-plugins": {
"cweagans/composer-patches": true
}
},
"extra": {
"composer-exit-on-patch-failure": true,
"patches": {
"drupal/module_name": {
"Description of patch": "https://www.drupal.org/files/issues/2024-01-15/module-issue-1234567-8.patch",
"Local patch": "patches/custom-fix.patch"
}
},
"patchLevel": {
"drupal/core": "-p2"
}
}
}
Upgrading from 1.x to 2.x
If you're on version 1.x and experiencing patch failures:
composer require cweagans/composer-patches:^2.0 --with-all-dependencies
Key differences in 2.x:
- Uses
git apply instead of patch binary (more reliable)
enable-patching option removed (patching is always enabled)
- Better error messages and debugging
- CRITICAL: Hash-based caching - patches may not re-apply if module already installed
Verifying Patches Are Applied
PROBLEM: composer-patches 2.x caches patch hashes in composer.lock. If modules are reinstalled or vendor updates occur without proper patch application, patches can silently go missing.
SOLUTION: Use a verify-patches.sh post-install hook:
./scripts/verify-patches.sh
./scripts/verify-patches.sh --fix
This script runs automatically after composer install and verifies critical patches are applied by checking for expected code patterns.
Adding New Critical Patches to Verification:
Edit scripts/verify-patches.sh and add to the CRITICAL_PATCHES array:
CRITICAL_PATCHES=(
"docroot/modules/contrib/MODULE:src/File.php:patternToFind:Description"
)
When Patches Go Missing:
- Run
./scripts/verify-patches.sh to identify missing patches
- Run
./scripts/verify-patches.sh --fix to auto-reinstall affected modules
- Or manually:
composer reinstall drupal/module_name
- If still failing, update
composer.lock: composer update --lock
Finding Patches
Issue Queue Search: https://www.drupal.org/project/issues/MODULE_NAME?categories=All
Patch Naming Convention:
- Format:
module-issue-NODEID-COMMENT.patch
- Example:
audiofield-d11-3432063-12.patch
- Node ID is the issue number (visit
drupal.org/node/NODEID)
When Existing Patches Fail After Update:
- Extract node ID from patch filename (e.g.,
3432063 from above)
- Visit
https://www.drupal.org/node/3432063
- Look for updated patch in latest comments
- Update composer.json with new patch URL
Debugging Errors: Find Patches BEFORE Creating
CRITICAL WORKFLOW: When encountering Drupal errors, ALWAYS search for existing patches before creating your own.
Step 1: Extract the Exact Error Signature
From the error message, extract the exact error string:
TypeError: Unsupported operand types: array + null in Drupal\field_ui\Form\EntityViewDisplayEditForm
"Unsupported operand types: array + null"
Step 2: Search Drupal.org Issue Queue FIRST
https://www.drupal.org/project/drupal/issues?text=Unsupported+operand+types+array+null
https://www.drupal.org/project/drupal/issues?text=EntityViewDisplayEditForm+line+166
What to look for in search results:
- Issues with status: "Needs review" or "Reviewed & tested by the community" (RTBC)
- Recent activity (check dates)
- Patch files in comments (look for
.patch attachments)
- Merge requests (look for
!13611 references)
Step 3: Use WebFetch to Get Patch Details
WebFetch(https://www.drupal.org/project/drupal/issues/3552531)
Look for:
- Patch file URLs: Usually
https://www.drupal.org/files/issues/YYYY-MM-DD/filename.patch
- Merge request numbers: E.g.,
!13611 → https://git.drupalcode.org/project/drupal/-/merge_requests/13611
- Issue status: RTBC means ready to use
Step 4: Download and Apply Official Patch
curl -O https://www.drupal.org/files/issues/2025-10-16/field-ui--unsupported-operand-types--3552531-2.patch
mv field-ui--unsupported-operand-types--3552531-2.patch patches/
{
"extra": {
"patches": {
"drupal/core": {
"Fix TypeError: Unsupported operand types array + null in EntityViewDisplayEditForm - Issue #3552531": "patches/field-ui--unsupported-operand-types--3552531-2.patch"
}
}
}
}
composer install
Common Search Patterns
| Error Type | Search Term |
|---|
| TypeError | Exact error message in quotes |
| Deprecated function | Function name (e.g., user_roles) |
| Missing method | Class name + method name |
| Fatal error | Exact error text |
Why This Matters
- Saves time: Don't recreate existing solutions
- Better quality: Community-reviewed patches are more robust
- Upstream integration: Using official patches means easier upgrades
- Documentation: Issue threads contain context and discussion
Anti-Pattern Example
❌ What NOT to do:
- See error
- Read code
- Create patch
- Apply patch
- (Someone points out existing issue)
✅ What TO do:
- See error
- Extract exact error message
- Search drupal.org issue queue
- Find existing patch
- Apply official patch
Creating Local Patches
IMPORTANT: Always create patches from a separate clone of the contrib module repo, not from the installed version in your project.
cd ~/Sites
git clone git@git.drupal.org:project/module_name.git module_name-contrib
cd ~/Sites/module_name-contrib
git checkout 1.0.3
git diff > ~/Sites/your-project/patches/module_name-custom-fix.patch
{
"extra": {
"patches": {
"drupal/module_name": {
"Custom fix description": "patches/module_name-custom-fix.patch"
}
}
}
}
composer reinstall drupal/module_name
Why use a separate repo?
- Creates clean patches without local modifications bleeding in
- Matches the exact file structure composer expects
- Allows proper version tracking with git tags
- Enables contributing patches upstream to drupal.org
Patch format: Patches should use git diff format (includes a/ and b/ prefixes):
diff --git a/src/File.php b/src/File.php
index abc123..def456 100644
--- a/src/File.php
+++ b/src/File.php
Patch Application
composer install
composer install
composer update drupal/module_name
composer patches-repatch
For detailed patch workflows, see: references/drupal-patches-workflow.md
Drupal 11 Compatibility Workflow
Step 1: Analyze Readiness
drush upgrade_status:analyze --all
drush upgrade_status:analyze module1 module2 module3
drush upgrade_status:analyze --all --format=json > d11-report.json
drush upgrade_status:analyze --all --format=codeclimate > d11-report-ci.json
drush upgrade_status:analyze --all --ignore-contrib
drush upgrade_status:analyze --all --ignore-custom
Step 2: Identify Issues
Major Issues (blocking):
REQUEST_TIME constant → Use \Drupal::time()->getRequestTime()
user_roles() → Use \Drupal\user\Entity\Role::loadMultiple()
file_validate_extensions() → Use file.validator service
system_retrieve_file() → No replacement (refactor required)
_drupal_flush_css_js() → Use AssetQueryStringInterface::reset()
Info.yml Issues:
- Update
core_version_requirement to include ^11
- Example:
core_version_requirement: ^9 || ^10 || ^11
Step 3: Fix Custom Code
Example: Inject Time Service
use Drupal\Core\Datetime\TimeInterface;
class MyController extends ControllerBase {
protected $time;
public function __construct(TimeInterface $time) {
$this->time = $time;
}
public static function create(ContainerInterface $container) {
return new static(
$container->get('datetime.time')
);
}
public function myMethod() {
$timestamp = $this->time->getRequestTime();
}
}
Example: Replace user_roles()
$roles = user_roles(TRUE);
use Drupal\user\Entity\Role;
$roles = Role::loadMultiple();
$role_options = [];
foreach ($roles as $role_id => $role) {
if ($role_id !== 'anonymous') {
$role_options[$role_id] = $role->label();
}
}
Step 4: Create .info.yml Patches
cd docroot/modules/contrib/module_name
git diff module.info.yml > /path/to/patches/module-d11-info.patch
--- a/module.info.yml
+++ b/module.info.yml
@@ -2,7 +2,7 @@
name: Module Name
type: module
description: Module description
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^9 || ^10 || ^11
Step 5: Apply Patches & Update Lenient List
{
"extra": {
"patches": {
"drupal/module_name": {
"Drupal 11 .info.yml support": "patches/module-d11-info.patch"
}
},
"drupal-lenient": {
"allowed-list": [
"drupal/module_name"
]
}
}
}
composer install
drush updb -y
drush cr
Step 6: Verify Fixes
drush upgrade_status:analyze module_name
Complete Update Checklist
Troubleshooting
Patch Won't Apply
composer show drupal/module_name
Version Conflict
Patch Already Applied
Database Update Fails
drush pm:uninstall module_name
composer require drupal/module_name --with-all-dependencies
drush pm:enable module_name
drush updb -y
Best Practices
- Always use
--with-all-dependencies for module updates
- Always run
drush updb after composer updates
- Test immediately after updates (visit pages, check logs)
- Keep patches organized in a
patches/ directory
- Document patches with descriptive names and comments
- Check issue queues first before creating custom patches
- Use upgrade_status to validate D11 compatibility
- Commit atomically: one module update per commit
- Use descriptive commit messages with patch references
- Keep drupal-lenient list minimal (only when necessary)
Production Deployment
When deploying to production environments, always optimize the Composer install:
composer install --no-dev -o
Why This Matters:
--no-dev reduces codebase size by excluding testing/dev tools
-o creates optimized class maps for faster autoloading
- Reduces security surface by excluding dev dependencies
- Improves performance on production servers
Production Deployment Workflow:
composer update drupal/module_name --with-all-dependencies
composer install --no-dev -o
git add composer.json composer.lock vendor/
git commit -m "Update module_name with production optimization"
git push origin master
ssh user@remote.server "cd /path/to/drupal && drush cr"
NEVER commit vendor/ with dev dependencies to production branches!
Developing Contrib Modules Locally
When actively developing a contrib module for drupal.org, use this workflow to avoid constantly updating via composer:
Symlink Development Workflow
cd /tmp
git clone git@git.drupal.org:project/module_name.git
cd module_name
cd /path/to/project
rm -rf docroot/modules/contrib/module_name
ln -s /tmp/module_name docroot/modules/contrib/module_name
drush cr
cd /tmp/module_name
git add -A
git commit -m "Your changes"
git push origin 1.0.x
cd /path/to/project
rm docroot/modules/contrib/module_name
composer install
Benefits:
- Test changes immediately without composer update cycles
- Keep git history in the module's own repo
- Easy to commit and push changes
- No risk of accidentally committing module code to main project
Important Notes:
- Don't forget to remove the symlink before committing project changes
- Clear Drupal cache after changes:
drush cr
- When done developing, always reinstall via composer to ensure clean state
- Useful for fixing autoloader issues, adding features, or troubleshooting
Common Patterns
Pattern: Update Module with Known Patch
composer require drupal/module_name:^3.0 --with-all-dependencies
drush updb -y
drush cr
git add composer.json composer.lock patches/
git commit -m "Update module_name to 3.0 with D11 compatibility patch"
Pattern: Fix Contrib D11 Issue
drush upgrade_status:analyze module_name
cd docroot/modules/contrib/module_name
git diff module.info.yml > ../../../patches/module-d11-info.patch
composer install
drush cr
drush upgrade_status:analyze module_name
Pattern: Major Version Upgrade with Breaking Changes
drush sql:dump > backup-before-update.sql
composer require drupal/module_name:^3.0 --with-all-dependencies
drush updb -y
drush watchdog:show --severity=Error --count=20
Contributing Back to drupal.org
When you've developed a fix or feature that should be contributed upstream, use the issue fork workflow.
Step 1: Create Issue on drupal.org
- Go to
https://www.drupal.org/project/issues/MODULE_NAME
- Click "Create a new issue"
- Fill in:
- Title: Descriptive title of the feature/fix
- Category: Bug report, Feature request, or Task
- Priority: Normal (unless exceptional)
- Note the issue number (e.g., 3569725)
Issue Description Format
Use the standard drupal.org template with HTML formatting:
<h3 id="overview">Overview</h3>
<p>Problem description here.</p>
<ul>
<li>Bullet point one</li>
<li>Bullet point two</li>
</ul>
<h3 id="proposed-resolution">Proposed resolution</h3>
<p><strong>Behavior:</strong></p>
<ul>
<li>Feature behavior one</li>
<li>Feature behavior two</li>
</ul>
<p><strong>Technical implementation:</strong></p>
<ul>
<li><code>SomeClass</code> - description</li>
<li><code>some_function()</code> - description</li>
</ul>
<p><strong>Files changed:</strong></p>
<ul>
<li><code>path/to/file.php</code> - Description of changes</li>
</ul>
<h3 id="ui-changes">User interface changes</h3>
<p>Description of UI changes (or "None" if no UI changes).</p>
<h3 id="steps-to-test">Steps to test</h3>
<ol>
<li>First step</li>
<li>Second step</li>
<li>Expected result</li>
</ol>
Formatting reference: https://www.drupal.org/filter/tips
<code>...</code> for inline code
<strong>...</strong> for bold
<ul><li>...</li></ul> for unordered lists
<ol><li>...</li></ol> for ordered lists
<h3 id="section-name">...</h3> for section headers
<p>...</p> for paragraphs
Step 2: Create Issue Fork on drupal.org
- On the issue page, click "Create issue fork"
- Copy the Git commands provided
Step 3: Clone Module and Set Up Fork
cd ~/Sites
git clone git@git.drupal.org:project/module_name.git module_name-contrib
cd module_name-contrib
git remote add module_name-XXXXXXX git@git.drupal.org:issue/module_name-XXXXXXX.git
git fetch module_name-XXXXXXX
git checkout -b 'XXXXXXX-short-description' --track module_name-XXXXXXX/'XXXXXXX-short-description'
Step 4: Make Changes and Test
Step 5: Commit and Push
git add path/to/changed/files
git commit -m "$(cat <<'EOF'
Issue #XXXXXXX: Short description
- Bullet point of change 1
- Bullet point of change 2
- Bullet point of change 3
EOF
)"
git push module_name-XXXXXXX XXXXXXX-short-description
Step 6: Create Merge Request
After pushing, you'll see a URL in the output:
remote: To create a merge request for XXXXXXX-short-description, visit:
remote: https://git.drupalcode.org/issue/module_name-XXXXXXX/-/merge_requests/new?merge_request%5Bsource_branch%5D=XXXXXXX-short-description
- Visit that URL to create the merge request
- Return to the issue page on drupal.org
- Set issue status to "Needs review"
Commit Message Format
Drupal.org standard format:
Issue #XXXXXXX: Short description (50 chars max)
- Detail about what changed
- Another detail
- Technical implementation note
Two-Repository Workflow
When contributing to a module you also use in your project:
- Contrib Repo (
~/Sites/module-contrib/) - Clean checkout for developing and contributing
- App Repo (
~/Sites/your-app/) - Uses composer patches to apply changes
Benefits:
- Clean separation between contribution work and app usage
- Patches can be applied/removed easily via Composer
- App stays functional while iterating on the feature
Workflow:
cd ~/Sites/module-contrib
git diff > feature-name.patch
cp feature-name.patch ~/Sites/your-app/patches/
cd ~/Sites/your-app
composer reinstall drupal/module_name
cd ~/Sites/module-contrib
git add -A && git commit -m "Issue #XXXXXXX: Description"
git push fork-remote branch-name
Using Remote Patches (After MR Created)
Once a merge request exists, you can use the remote diff URL:
{
"extra": {
"patches": {
"drupal/module_name": {
"Feature (https://www.drupal.org/project/module_name/issues/XXXXXXX)": "https://git.drupalcode.org/project/module_name/-/merge_requests/XXX.diff"
}
}
}
}
Reference Links