
Drupal Contrib Mgmt
- 158 installs
- 73 repo stars
- Updated July 30, 2026
- grasmash/drupal-claude-skills
Patch Drupal contrib modules and clear deprecation findings when upstream fixes are missing.
About
drupal-contrib-mgmt is a procedural agent skill for solo builders and small teams running Drupal sites that depend on contrib modules. It walks through creating local patches when Drupal.org has no merged fix yet—starting with drush upgrade_status analysis, confirming a clean contrib module git checkout, editing the flagged PHP (such as swapping deprecated user_roles() for Role entity loading), and preparing patch-ready diffs. The skill fits maintainers who ship on Composer-managed Drupal and need repeatable, auditable contrib fixes without waiting on upstream releases. Use it during deprecation sweeps before major core upgrades, when CI or Upgrade Status blocks deploys, or when licensing-style modules still call removed APIs. It complements standard Composer and patch plugin workflows by making agent-guided edits consistent with Drupal coding standards and issue-queue expectations.
- Step-by-step custom patch workflow when no issue-queue patch exists
- Uses drush upgrade_status:analyze to pinpoint deprecated API usage (e.g. user_roles)
- Documents replacing globals with injected Drupal\user\Entity\Role::loadMultiple() patterns
- Requires a clean git tree under docroot/modules/contrib before editing
- Pairs verification with module-specific deprecation output before patch export
Drupal Contrib Mgmt by the numbers
- 158 all-time installs (skills.sh)
- Ranked #40 of 65 PHP & Laravel skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/grasmash/drupal-claude-skills --skill drupal-contrib-mgmtAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 158 |
|---|---|
| repo stars | ★ 73 |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 30, 2026 |
| Repository | grasmash/drupal-claude-skills ↗ |
What it does
Patch Drupal contrib modules and clear deprecation findings when upstream fixes are missing.
Files
Drupal Contrib Module Management
Core Update Workflow
Standard Module Update
# Update a single module
composer require drupal/module_name --with-all-dependencies
# Update to specific version
composer require drupal/module_name:^3.0 --with-all-dependencies
# Update multiple modules
composer require drupal/module_a drupal/module_b --with-all-dependencies
# After any update, ALWAYS run database updates
drush updb -y
# Clear cache if needed
drush cr
# CRITICAL: Test by visiting pages to check for fatal errors
# Visit at least one page that uses the updated moduleMajor Version Upgrades
When upgrading to a new major version (e.g., 2.x → 3.x):
1. Check compatibility: Ensure module supports your Drupal core version 2. Search issue queue for patches: https://www.drupal.org/project/issues/MODULE_NAME?categories=All 3. Use Drupal Lenient for version requirement issues (see below) 4. Apply patches via composer.json (see Patch Management section) 5. 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)
# Check the module's .info.yml file for core_version_requirement
cat docroot/modules/contrib/MODULE_NAME/MODULE_NAME.info.yml | grep core_version_requirementWhat to look for:
core_version_requirement: ^9.5 || ^10 || ^11 # ✅ D11 compatible
core_version_requirement: ^8 || ^9 || ^10 || ^11 # ✅ D11 compatible
core_version_requirement: ^9 || ^10 # ❌ Not D11 compatible yetExample:
$ cat docroot/modules/contrib/admin_toolbar/admin_toolbar.info.yml | grep core_version
core_version_requirement: ^9.5 || ^10 || ^11
# ✅ This module declares D11 support!Method 2: Use Composer Commands (Works Before Installing)
# Check what versions are available and their constraints
composer show drupal/MODULE_NAME --all | grep -A5 "^versions"
# Check currently installed version
composer show drupal/MODULE_NAME | grep versionsWhat 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_NAMELook 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
^11support, module maintainer says it works
Real-World Examples:
# admin_toolbar - Already D11 compatible
$ cat docroot/modules/contrib/admin_toolbar/admin_toolbar.info.yml | grep core_version
core_version_requirement: ^9.5 || ^10 || ^11
# But upgrade_status shows warnings about _drupal_flush_css_js()
# This is a FALSE POSITIVE - module handles it with version checks
# audiofield - Already D11 compatible
$ cat docroot/modules/contrib/audiofield/audiofield.info.yml | grep core_version
core_version_requirement: ^8 || ^9 || ^10 || ^11
# Has deprecation warnings but maintainer declares D11 supportDrupal 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
# Add module to allowed-list, then install
composer require drupal/module_name --with-all-dependenciesPatch 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-dependenciesKey differences in 2.x:
- Uses
git applyinstead ofpatchbinary (more reliable) enable-patchingoption removed (patching is always enabled)- Better error messages and debugging
- CRITICAL — the `patches.lock.json` apply source: v2 applies patches from
patches.lock.jsononcomposer install/composer reinstall. It does NOT readextra.patchesincomposer.jsonduring those commands — onlycomposer updateandcomposer patches-relockre-readcomposer.jsonand regenerate the lock. So adding a patch tocomposer.jsonand runningcomposer installapplies nothing for that patch until you relock. This is the #1 cause of patches that "keep regressing": local dev looks fixed (you hand-applied it or ranupdate), but the next clean install — CI, a teammate, a fresh deploy — reads the stale lock and drops the patch. Always run `composer patches-relock` after editing `extra.patches`, and commit `patches.lock.json`.
Verifying Patches Are Applied
THREE DIFFERENT PROBLEMS, ONE SCRIPT:
1. Lock-sync staleness (the root cause): a patch is registered in composer.json extra.patches but never added to patches.lock.json because composer patches-relock was skipped. v2 applies from the lock on composer install, so the patch is silently a no-op on every clean install. The fix is the relock; the script's job is to catch the skip by asserting every local patch in composer.json is present in patches.lock.json.
2. Committed file drift: a patch IS applied to the working tree, but the resulting contrib file change is never committed to git. Pantheon (and any platform that deploys from committed git state without running composer install) never sees it, so production silently runs un-patched code. Local dev looks fine. See CLAUDE.md "Contrib/Core Patch Policy" for context.
3. Patch hash cache staleness: even with the lock in sync, a stray reinstall or vendor update can skip re-applying. Rare next to (1) and (2), but the same materialized-file check catches it.
SOLUTION: scripts/verify-patches.sh
# Run manually (verifies committed state)
./scripts/verify-patches.sh
# Auto-reinstall affected modules to re-apply patches
./scripts/verify-patches.sh --fixBehavior:
- Runs two checks. (1) Lock-sync: every local patch in
composer.jsonextra.patchesmust also appear inpatches.lock.json— catches the skippedpatches-relock. (2) Materialized-file: the patched lines must be present in the committed contrib file — catches "patched but not committed". - Auto-derives the verification list from
composer.jsonextra.patches— no manual curation required. Adding a patch entry is enough; the script picks it up automatically. - For each local patch (value starting with
patches/), it parses all+++ b/<path>headers, extracts up to 5 distinctive added lines (≥ 8 non-whitespace chars, not a substring of any-line in the same patch), and greps the target file for them. Handles thedrupal/corepackage'score/path-prefix quirk and is bash 3 compatible. - URL-based patches (
https://...) are skipped with a notice — add a local mirror underpatches/if the patch is critical. - Runs in CI before
composer installin thelintjob (.github/workflows/test.yml), so it validates the COMMITTED tree — not the post-install state. This is the ordering that matters.
Adding a new patch (the relock step is the one everyone forgets): 1. Drop the .patch file in patches/ 2. Register it in composer.json under extra.patches 3. Run `composer patches-relock` — adds the patch to patches.lock.json. WITHOUT this, step 4's composer install applies nothing (v2 reads the lock, not composer.json). 4. Run composer install to apply the patch to the working tree 5. `git add` and commit the modified contrib file along with composer.json, patches.lock.json, and the new .patch file — platforms that deploy from git (Pantheon) can't apply patches on their own, so the committed contrib file must already be in its patched form 6. Run ./scripts/verify-patches.sh locally to sanity-check before pushing 7. CI will re-run the same verification on every push
When `verify-patches.sh` reports MISSING in CI:
- Lock-sync failure → someone skipped
composer patches-relock(step 3). Fix: run it, commitpatches.lock.json, push. - Materialized-file failure → someone forgot to commit the patched contrib file (step 5). Fix:
composer patches-relock && composer installlocally,git add docroot/modules/contrib docroot/core patches.lock.json, commit, and push.
Caveats:
- "Combined patches" (one
.patchfile with multiple+++ b/<same_file>headers, usually squashed commits with conflicting hunks) may slip through — the script accepts any distinctive added line, so a partial match passes. If you see a patch land inpatches/with multiple hunks revising the same file, regenerate it as a clean single-commit diff instead. - PHPCS: committing patched contrib files can trip
grumphp's pre-commitphpcstask on pre-existing sniff violations in upstream code.grumphp.ymlalready ignoresdocroot/modules/contrib,docroot/core, anddocroot/librariesfor this task — don't remove those ignores.
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: 1. Extract node ID from patch filename (e.g., 3432063 from above) 2. Visit https://www.drupal.org/node/3432063 3. Look for updated patch in latest comments 4. 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:
# Example error:
TypeError: Unsupported operand types: array + null in Drupal\field_ui\Form\EntityViewDisplayEditForm
# Extract this part:
"Unsupported operand types: array + null"Step 2: Search Drupal.org Issue Queue FIRST
# Method 1: Direct URL search (BEST)
https://www.drupal.org/project/drupal/issues?text=Unsupported+operand+types+array+null
# Method 2: Search with file + line number
https://www.drupal.org/project/drupal/issues?text=EntityViewDisplayEditForm+line+166What 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
.patchattachments) - Merge requests (look for
!13611references)
Step 3: Use WebFetch to Get Patch Details
# Once you find the issue, fetch 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
# Download to patches directory
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/
# Add to composer.json with descriptive name referencing issue
{
"extra": {
"patches": {
"drupal/core": {
"Fix TypeError: Unsupported operand types array + null in EntityViewDisplayEditForm - Issue #3552531": "patches/field-ui--unsupported-operand-types--3552531-2.patch"
}
}
}
}
# Apply
composer installCommon 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: 1. See error 2. Read code 3. Create patch 4. Apply patch 5. (Someone points out existing issue)
✅ What TO do: 1. See error 2. Extract exact error message 3. Search drupal.org issue queue 4. Find existing patch 5. 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.
# Step 1: Clone the module repo to a separate directory (one-time setup)
cd ~/Sites
git clone git@git.drupal.org:project/module_name.git module_name-contrib
# Step 2: Checkout the exact version you have installed
cd ~/Sites/module_name-contrib
git checkout 1.0.3 # Match your installed version
# Step 3: Make your changes in the contrib repo
# Edit files as needed...
# Step 4: Generate the patch using git diff
git diff > ~/Sites/your-project/patches/module_name-custom-fix.patch
# Step 5: Add to composer.json
{
"extra": {
"patches": {
"drupal/module_name": {
"Custom fix description": "patches/module_name-custom-fix.patch"
}
}
}
}
# Step 6: Apply via composer
composer reinstall drupal/module_nameWhy 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.phpPatch Application
# Install with patches
composer install
# If patches fail, composer will error
# Update or remove failing patches, then retry
composer install
# Re-patch a single module (most common)
composer update drupal/module_name
# Re-patch ALL patched dependencies (use when changing multiple patches)
composer patches-repatchFor detailed patch workflows, see: references/drupal-patches-workflow.md
Drupal 11 Compatibility Workflow
Step 1: Analyze Readiness
# Scan all modules
drush upgrade_status:analyze --all
# Scan specific modules
drush upgrade_status:analyze module1 module2 module3
# Machine-readable output
drush upgrade_status:analyze --all --format=json > d11-report.json
drush upgrade_status:analyze --all --format=codeclimate > d11-report-ci.json
# Scan only custom code
drush upgrade_status:analyze --all --ignore-contrib
# Scan only contrib
drush upgrade_status:analyze --all --ignore-customStep 2: Identify Issues
Major Issues (blocking):
REQUEST_TIMEconstant → Use\Drupal::time()->getRequestTime()user_roles()→ Use\Drupal\user\Entity\Role::loadMultiple()file_validate_extensions()→ Usefile.validatorservicesystem_retrieve_file()→ No replacement (refactor required)_drupal_flush_css_js()→ UseAssetQueryStringInterface::reset()
Info.yml Issues:
- Update
core_version_requirementto 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() {
// OLD: $timestamp = REQUEST_TIME;
$timestamp = $this->time->getRequestTime();
}
}Example: Replace user_roles()
// OLD:
$roles = user_roles(TRUE);
// NEW:
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
# Create patch for contrib module
cd docroot/modules/contrib/module_name
git diff module.info.yml > /path/to/patches/module-d11-info.patch
# Patch content:
--- 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 || ^11Step 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 crStep 6: Verify Fixes
# Re-scan to confirm issues resolved
drush upgrade_status:analyze module_name
# Should show "No known issues found"Complete Update Checklist
- [ ] Check current module version:
composer show drupal/module_name - [ ] Search issue queue for known issues
- [ ] Check if module is D11 compatible
- [ ] Update composer.json with new version
- [ ] Add to drupal-lenient if needed
- [ ] Search for and apply necessary patches
- [ ] Run
composer require drupal/module_name:^X.0 --with-all-dependencies - [ ] Run
drush updb -y - [ ] Run
drush cr - [ ] Run
drush upgrade_status:analyze module_name - [ ] Test module functionality by visiting relevant pages
- [ ] Check for PHP errors/warnings in logs
- [ ] Commit changes with descriptive message
Troubleshooting
Patch Won't Apply
# Error: "Cannot apply patch..."
# 1. Check if module version changed
composer show drupal/module_name
# 2. Search issue queue for updated patch
# Visit drupal.org/node/NODEID (from patch filename)
# 3. Update composer.json with new patch URL
# 4. Or remove patch if merged upstreamVersion Conflict
# Error: "drupal/module_name requires drupal/core ^9"
# Add to drupal-lenient allowed-listPatch Already Applied
# Error: "patch ... has already been applied"
# Module maintainer merged the patch - remove from composer.jsonDatabase Update Fails
# Error during drush updb
# 1. Check error message carefully
# 2. May need to disable module, update, re-enable
drush pm:uninstall module_name
composer require drupal/module_name --with-all-dependencies
drush pm:enable module_name
drush updb -yBest Practices
1. Always use `--with-all-dependencies` for module updates 2. Always run `drush updb` after composer updates 3. Test immediately after updates (visit pages, check logs) 4. Keep patches organized in a patches/ directory 5. Document patches with descriptive names and comments 6. Check issue queues first before creating custom patches 7. Use upgrade_status to validate D11 compatibility 8. Commit atomically: one module update per commit 9. Use descriptive commit messages with patch references 10. Keep drupal-lenient list minimal (only when necessary)
Production Deployment
When deploying to production environments (Pantheon, Acquia, etc.), always optimize the Composer install:
# CRITICAL: Always use these flags for production
composer install --no-dev -o
# --no-dev: Excludes development dependencies (phpunit, rector, etc.)
# -o (--optimize-autoloader): Optimizes autoloader for performanceWhy This Matters:
--no-devreduces codebase size by excluding testing/dev tools-ocreates optimized class maps for faster autoloading- Reduces security surface by excluding dev dependencies
- Improves performance on production servers
Production Deployment Workflow:
# 1. After making composer changes locally
composer update drupal/module_name --with-all-dependencies
# 2. Before committing, optimize for production
composer install --no-dev -o
# 3. Commit the optimized vendor files
git add composer.json composer.lock vendor/
git commit -m "Update module_name with production optimization"
# 4. Push to production
git push origin master
# 5. Rebuild caches on the remote env (use your platform's remote-drush form):
acli remote:drush -- cr # Acquia
# terminus drush <site>.<env> -- cr # Pantheon
# platform drush -e <env> -- cr # Platform.sh (Upsun: upsun drush -- cr)
# lagoon ssh -p <project> -e <env> -C "drush cr" # Lagoon / amazee.io
# drush @<alias> cr # generic, any host with Drush aliasesNEVER 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
# 1. Set up module repository in temp location
cd /tmp
git clone git@git.drupal.org:project/module_name.git
cd module_name
# Make your changes...
# 2. Remove composer-installed version and symlink your dev copy
cd /path/to/project
rm -rf docroot/modules/contrib/module_name
ln -s /tmp/module_name docroot/modules/contrib/module_name
# 3. Develop and test
# Make changes in /tmp/module_name
# Test immediately in your Drupal site
drush cr # Clear cache as needed
# 4. When ready to publish
cd /tmp/module_name
git add -A
git commit -m "Your changes"
git push origin 1.0.x
# 5. Clean up: remove symlink and reinstall from composer
cd /path/to/project
rm docroot/modules/contrib/module_name
composer install # Reinstalls from drupal.orgBenefits:
- 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
Example: Fixing recurly_commerce_api autoloader issue
# Module needed composer.json autoload section
cd /tmp/recurly_commerce_api
# Edit composer.json to add autoload section
git commit -m "Add PSR-4 autoload configuration"
git push origin 1.0.x
# Back in main project
rm docroot/modules/contrib/recurly_commerce_api
composer install # Gets latest with fix
drush crCommon Patterns
Pattern: Update Module with Known Patch
# 1. Find patch in issue queue
# 2. Add to composer.json patches section
# 3. Update module
composer require drupal/module_name:^3.0 --with-all-dependencies
drush updb -y
drush cr
# 4. Test
# 5. Commit
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
# 1. Scan for issues
drush upgrade_status:analyze module_name
# 2. Create info.yml patch if needed
cd docroot/modules/contrib/module_name
# Edit module.info.yml to add ^11
git diff module.info.yml > ../../../patches/module-d11-info.patch
# 3. Add patch to composer.json
# 4. Apply
composer install
drush cr
# 5. Verify
drush upgrade_status:analyze module_namePattern: Major Version Upgrade with Breaking Changes
# 1. Read CHANGELOG/UPDATE.md for breaking changes
# 2. Check issue queue for upgrade path documentation
# 3. Backup database before upgrade
drush sql:dump > backup-before-update.sql
# 4. Update module
composer require drupal/module_name:^3.0 --with-all-dependencies
# 5. Run updates
drush updb -y
# 6. Check for errors
drush watchdog:show --severity=Error --count=20
# 7. Test thoroughly
# 8. If issues, can rollback:
# git checkout composer.json composer.lock
# composer install
# drush sql:cli < backup-before-update.sqlContributing 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
1. Go to https://www.drupal.org/project/issues/MODULE_NAME 2. Click "Create a new issue" 3. Fill in:
- Title: Descriptive title of the feature/fix
- Category: Bug report, Feature request, or Task
- Priority: Normal (unless exceptional)
4. 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
1. On the issue page, click "Create issue fork" 2. Copy the Git commands provided
Step 3: Clone Module and Set Up Fork
# Clone the module repo (if not already cloned)
cd ~/Sites
git clone git@git.drupal.org:project/module_name.git module_name-contrib
cd module_name-contrib
# Add the issue fork as a remote (replace XXXXXXX with issue number)
git remote add module_name-XXXXXXX git@git.drupal.org:issue/module_name-XXXXXXX.git
git fetch module_name-XXXXXXX
# Checkout the issue branch
git checkout -b 'XXXXXXX-short-description' --track module_name-XXXXXXX/'XXXXXXX-short-description'Step 4: Make Changes and Test
# Make your changes
# For PHP modules, ensure code follows Drupal coding standards
# For modules with JS/UI, run linting and build
# Test your changes locallyStep 5: Commit and Push
# Stage changed files
git add path/to/changed/files
# Commit with proper message format
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
)"
# Push to issue fork
git push module_name-XXXXXXX XXXXXXX-short-descriptionStep 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-description1. Visit that URL to create the merge request 2. Return to the issue page on drupal.org 3. 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 noteTwo-Repository Workflow
When contributing to a module you also use in your project:
1. Contrib Repo (~/Sites/module-contrib/) - Clean checkout for developing and contributing 2. 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:
# 1. Develop in contrib repo
cd ~/Sites/module-contrib
# Make changes...
# 2. Generate patch
git diff > feature-name.patch
# 3. Copy to app and apply via composer
cp feature-name.patch ~/Sites/your-app/patches/
# Add to composer.json patches section
cd ~/Sites/your-app
composer reinstall drupal/module_name
# 4. Test in app, iterate as needed
# 5. When ready, commit and push from contrib repo
cd ~/Sites/module-contrib
git add -A && git commit -m "Issue #XXXXXXX: Description"
git push fork-remote branch-nameUsing 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
- Composer Patches: https://github.com/cweagans/composer-patches
- Drupal Lenient: https://github.com/mglaman/composer-drupal-lenient
- Upgrade Status Module: https://www.drupal.org/project/upgrade_status
- Drupal 11 Deprecations: https://www.drupal.org/about/core/policies/core-change-policies/drupal-deprecation-policy
- Patch Naming Standards: https://www.drupal.org/node/1054616
- Creating Issue Forks: https://www.drupal.org/docs/develop/git/using-gitlab-to-contribute-to-drupal/creating-issue-forks
- Issue Report Guide: https://www.drupal.org/community/contributor-guide/reference-information/quick-info/creating-or-updating-an-issue-report
- Text Formatting Tips: https://www.drupal.org/filter/tips
- Git Workflow for Drupal: https://www.drupal.org/docs/develop/git/using-git-to-contribute-to-drupal
#!/bin/bash
# Example: Create a custom patch when no upstream patch exists
# SCENARIO: licensing module uses deprecated user_roles() function
# No patch exists in issue queue, so we create our own
echo "Creating custom patch for licensing module user_roles() deprecation"
# Step 1: Verify the issue
echo "Step 1: Checking current code..."
drush upgrade_status:analyze licensing
# Shows: "Call to deprecated function user_roles() at line 77"
# Step 2: Navigate to module
cd docroot/modules/contrib/licensing
# Step 3: Check git status (should be clean)
git status
# Should show: nothing to commit, working tree clean
# Step 4: Identify the file and make changes
echo "Step 4: Editing src/Form/LicenseTypeForm.php..."
# BEFORE (line ~77):
# $role_options = user_roles(TRUE);
# AFTER: Use proper dependency injection
# Add to file header:
# use Drupal\user\Entity\Role;
# In the form builder method, replace:
# $role_options = user_roles(TRUE);
#
# With:
# $roles = Role::loadMultiple();
# $role_options = [];
# foreach ($roles as $role_id => $role) {
# if ($role_id !== 'anonymous') {
# $role_options[$role_id] = $role->label();
# }
# }
# For this example, let's show the actual edit:
cat > /tmp/licensing_patch_snippet.txt <<'EOF'
This is what you would edit in src/Form/LicenseTypeForm.php:
1. Add to top of file (around line 5):
use Drupal\user\Entity\Role;
2. Replace (around line 77):
OLD: $role_options = user_roles(TRUE);
NEW:
$roles = Role::loadMultiple();
$role_options = [];
foreach ($roles as $role_id => $role) {
if ($role_id !== 'anonymous') {
$role_options[$role_id] = $role->label();
}
}
EOF
cat /tmp/licensing_patch_snippet.txt
# After making your edits in your editor:
echo "
Make the changes shown above using your text editor, then continue...
Press Enter when changes are made"
# read -p ""
# Step 5: Create the patch
echo "Step 5: Creating patch file..."
git diff > ../../../patches/licensing-user-roles-d11-fix.patch
# Step 6: Verify patch format
echo "Step 6: Verifying patch..."
cat ../../../patches/licensing-user-roles-d11-fix.patch
# Should show:
# diff --git a/src/Form/LicenseTypeForm.php b/src/Form/LicenseTypeForm.php
# index abc123..def456 100644
# --- a/src/Form/LicenseTypeForm.php
# +++ b/src/Form/LicenseTypeForm.php
# ... changes ...
# Step 7: Test patch applies
echo "Step 7: Testing patch application..."
git apply --check ../../../patches/licensing-user-roles-d11-fix.patch
echo "✓ Patch applies cleanly"
# Step 8: Reset changes (patch will be applied via composer)
git checkout .
# Step 9: Add to composer.json
cd ../../..
echo "Step 9: Adding patch to composer.json..."
# Edit composer.json to add:
cat >> composer.json <<'EOF'
{
"extra": {
"patches": {
"drupal/licensing": {
"Replace deprecated user_roles() for D11 compatibility": "patches/licensing-user-roles-d11-fix.patch",
"Drupal 11 .info.yml support": "patches/licensing-d11-info.patch"
}
}
}
}
EOF
# Step 10: Apply via composer
echo "Step 10: Applying patch via composer..."
composer install
# Should show:
# - Applying patches for drupal/licensing
# patches/licensing-user-roles-d11-fix.patch (Replace deprecated user_roles()...)
# Step 11: Test
echo "Step 11: Testing..."
drush cr
drush upgrade_status:analyze licensing
# Should now show: "No known issues found"
# Test functionality
echo "
Manual testing:
1. Visit /admin/licensing/types
2. Add/edit a license type
3. Check role selection works
4. Save and verify
"
drush watchdog:show --severity=Error --count=10
# Step 12: Commit
git add composer.json composer.lock patches/licensing-user-roles-d11-fix.patch
git commit -m "Fix deprecated user_roles() in licensing module for D11
Created custom patch to replace deprecated user_roles() function with
Role::loadMultiple() pattern following Drupal best practices.
The patch:
- Adds proper use statement for Role entity
- Replaces user_roles(TRUE) with Role::loadMultiple()
- Filters out anonymous role manually
- Maintains same functionality
Tested: License type form works correctly, role selection functioning."
echo "✓ Custom patch created and applied!"
# BONUS: Consider contributing back
echo "
Optional: Contribute patch to drupal.org
1. Check if issue exists: https://www.drupal.org/project/issues/licensing
2. If not, create new issue:
- Title: 'Replace deprecated user_roles() for Drupal 11'
- Category: Bug report
- Priority: Normal
- Version: 2.0.x-dev
3. Upload your patch file
4. Explain changes and testing
5. Set status to 'Needs review'
"
#!/bin/bash
# Example: Find and apply a patch from Drupal.org issue queue
# SCENARIO: audiofield module has deprecated file_validate_extensions() function
# We need to find a patch to fix this for Drupal 11
# Step 1: Go to issue queue
echo "1. Navigate to: https://www.drupal.org/project/issues/audiofield?categories=All"
echo "2. Search for: 'file_validate_extensions' or 'Drupal 11'"
# Step 2: Evaluate the issue
echo "
Found issue: https://www.drupal.org/node/3432063
Title: 'Replace deprecated file_validate_extensions()'
Status: Needs review
✅ Tests passing (green checkmark)
✅ Multiple people tested
✅ Recent activity
"
# Step 3: Find the patch
echo "
Latest patch in comment #12:
audiofield-file-validator-3432063-12.patch
URL: https://www.drupal.org/files/issues/2024-06-15/audiofield-file-validator-3432063-12.patch
"
# Step 4: Test patch locally first (optional but recommended)
echo "Testing patch before adding to composer..."
cd docroot/modules/contrib/audiofield
curl -O https://www.drupal.org/files/issues/2024-06-15/audiofield-file-validator-3432063-12.patch
# Dry run to check if it applies
patch -p1 --dry-run < audiofield-file-validator-3432063-12.patch
echo "✓ Patch applies cleanly"
cd ../../..
# Step 5: Add to composer.json
cat >> composer.json <<'EOF'
{
"extra": {
"patches": {
"drupal/audiofield": {
"Replace deprecated file_validate_extensions() - https://drupal.org/node/3432063": "https://www.drupal.org/files/issues/2024-06-15/audiofield-file-validator-3432063-12.patch"
}
}
}
}
EOF
# Step 6: Apply patch via composer
composer install
# Or if module needs updating too:
# composer require drupal/audiofield:^1.13 --with-all-dependencies
# Step 7: Verify
drush cr
drush upgrade_status:analyze audiofield
echo "✓ Deprecation should be resolved"
# Step 8: Test functionality
echo "
Manual testing checklist:
- Visit audio field configuration
- Upload an audio file
- Check file validation works
- View node with audio field
- Check for PHP errors in logs
"
drush watchdog:show --severity=Error --count=10
# Step 9: Commit
git add composer.json composer.lock
git commit -m "Apply patch to fix file_validate_extensions() in audiofield
Applied patch from drupal.org/node/3432063 to fix deprecated
file_validate_extensions() function for Drupal 11 compatibility.
Tested: Audio upload and validation working correctly."
echo "✓ Complete!"
# ALTERNATIVE: If no patch exists in issue queue
echo "
If no suitable patch found:
1. Create your own (see create-custom-patch.sh example)
2. Consider contributing it back to the issue queue
3. Add comment to issue queue mentioning you're working on it
"
#!/bin/bash
# Example: Upgrade entity_limit from 2.x to 3.x with D11 compatibility
# 1. Check current version
composer show drupal/entity_limit
# Output: drupal/entity_limit 2.0.0
# 2. Search issue queue for known issues
# Visit: https://www.drupal.org/project/issues/entity_limit?categories=All
# Find: Issue #3432063 - Drupal calls should be avoided in classes
# 3. Add necessary patches and lenient configuration
cat >> composer.json <<'EOF'
{
"extra": {
"patches": {
"drupal/entity_limit": {
"Drupal calls should be avoided in classes": "https://www.drupal.org/files/issues/2024-03-19/3432063-2.patch",
"Drupal 11 .info.yml support": "patches/entity_limit-d11-info.patch"
}
},
"drupal-lenient": {
"allowed-list": [
"drupal/entity_limit"
]
}
}
}
EOF
# 4. Create .info.yml patch
cd docroot/modules/contrib/entity_limit
# Manually edit entity_limit.info.yml to add ^11 to core_version_requirement
git diff entity_limit.info.yml > ../../../patches/entity_limit-d11-info.patch
cd ../../..
# 5. Backup database (major version upgrade!)
drush sql:dump > backup-before-entity-limit-3x.sql
# 6. Update to 3.x
composer require drupal/entity_limit:^3.0@beta --with-all-dependencies
# 7. Run database updates
drush updb -y
# 8. Check for errors
drush watchdog:show --severity=Error --count=10
# 9. Clear cache
drush cr
# 10. Run upgrade_status check
drush upgrade_status:analyze entity_limit
# 11. Test functionality
# - Visit entity limit configuration page
# - Test creating content with entity limits
# - Check permissions work correctly
# 12. If successful, commit
git add composer.json composer.lock patches/entity_limit-d11-info.patch
git commit -m "Upgrade entity_limit to 3.0.0-beta1 with D11 compatibility
Breaking changes:
- Updated API methods (see https://www.drupal.org/node/XXXXX)
- New permission system
Applied patches:
- Drupal calls fix (#3432063)
- D11 core version requirement
Tested: All entity limit functionality working correctly"
# 13. If issues occur, rollback:
# git checkout composer.json composer.lock
# composer install
# drush sql:cli < backup-before-entity-limit-3x.sql
# drush cr
#!/bin/bash
# Example: Update audiofield module with D11 compatibility patch
# 1. Add patch to composer.json first
cat >> composer.json <<'EOF'
{
"extra": {
"patches": {
"drupal/audiofield": {
"Drupal 11 .info.yml support": "patches/audiofield-d11-info.patch",
"Fix file_validate_extensions deprecation": "https://www.drupal.org/files/issues/2024-06-15/audiofield-3432063-12.patch"
}
}
}
}
EOF
# 2. Create local .info.yml patch if needed
cd docroot/modules/contrib/audiofield
git diff audiofield.info.yml > ../../../patches/audiofield-d11-info.patch
cd ../../..
# 3. Update module
composer require drupal/audiofield:^1.13 --with-all-dependencies
# 4. Run database updates
drush updb -y
# 5. Clear cache
drush cr
# 6. Verify fix
drush upgrade_status:analyze audiofield
# 7. Test functionality
# Visit a page that uses audiofield to ensure no fatal errors
# 8. Commit
git add composer.json composer.lock patches/audiofield-d11-info.patch
git commit -m "Update audiofield to 1.13 with D11 compatibility patches
- Added Drupal 11 core_version_requirement support
- Applied patch for file_validate_extensions() deprecation
- Tested: audio upload functionality works correctly"
Common Drupal 11 Deprecations and Fixes
Deprecated Constants
REQUEST_TIME
Deprecated in: Drupal 8.3.0 Removed in: Drupal 11.0.0
OLD:
$timestamp = REQUEST_TIME;
$time_ago = REQUEST_TIME - $node->getCreatedTime();NEW:
// Inject TimeInterface service
use Drupal\Core\Datetime\TimeInterface;
class MyClass {
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();
$time_ago = $this->time->getRequestTime() - $node->getCreatedTime();
}
}Deprecated Functions
user_roles()
Deprecated in: Drupal 10.2.0 Removed in: Drupal 11.0.0
OLD:
$roles = user_roles(TRUE); // Exclude anonymousNEW:
use Drupal\user\Entity\Role;
$roles = Role::loadMultiple();
$role_names = [];
foreach ($roles as $role_id => $role) {
if ($role_id !== 'anonymous') {
$role_names[$role_id] = $role->label();
}
}user_role_names()
Deprecated in: Drupal 10.2.0 Removed in: Drupal 11.0.0
OLD:
$role_options = user_role_names(TRUE);NEW:
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();
}
}file_validate_extensions()
Deprecated in: Drupal 10.2.0 Removed in: Drupal 11.0.0
OLD:
$errors = file_validate_extensions($file, 'mp3 wav ogg');NEW:
// Inject file.validator service
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\Validation\FileValidatorInterface;
class MyClass {
protected $fileValidator;
public function __construct(FileValidatorInterface $file_validator) {
$this->fileValidator = $file_validator;
}
public static function create(ContainerInterface $container) {
return new static($container->get('file.validator'));
}
public function validateFile($file) {
$validators = [
'FileExtension' => [
'extensions' => 'mp3 wav ogg',
],
];
$violations = $this->fileValidator->validate($file, $validators);
return $violations;
}
}system_retrieve_file()
Deprecated in: Drupal 10.2.0 Removed in: Drupal 11.0.0 Replacement: None - must be refactored
OLD:
$file = system_retrieve_file($url, $destination, FALSE, FILE_EXISTS_REPLACE);NEW:
// Use file_system service and http_client
use Drupal\Core\File\FileSystemInterface;
use GuzzleHttp\ClientInterface;
class MyClass {
protected $fileSystem;
protected $httpClient;
public function __construct(FileSystemInterface $file_system, ClientInterface $http_client) {
$this->fileSystem = $file_system;
$this->httpClient = $http_client;
}
public static function create(ContainerInterface $container) {
return new static(
$container->get('file_system'),
$container->get('http_client')
);
}
public function retrieveFile($url, $destination) {
try {
$response = $this->httpClient->get($url);
$data = $response->getBody()->getContents();
$directory = dirname($destination);
$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
return $this->fileSystem->saveData($data, $destination, FileSystemInterface::EXISTS_REPLACE);
}
catch (\Exception $e) {
\Drupal::logger('my_module')->error('Failed to retrieve file: @error', ['@error' => $e->getMessage()]);
return FALSE;
}
}
}_drupal_flush_css_js()
Deprecated in: Drupal 10.2.0 Removed in: Drupal 11.0.0
OLD:
_drupal_flush_css_js();NEW:
// Inject asset.query_string service
use Drupal\Core\Asset\AssetQueryStringInterface;
class MyClass {
protected $assetQueryString;
public function __construct(AssetQueryStringInterface $asset_query_string) {
$this->assetQueryString = $asset_query_string;
}
public static function create(ContainerInterface $container) {
return new static($container->get('asset.query_string'));
}
public function flushAssets() {
$this->assetQueryString->reset();
}
}Deprecated Class Constants
FileSystemInterface::EXISTS_* Constants
Deprecated in: Drupal 10.3.0 Removed in: Drupal 12.0.0 (warning for D11)
OLD:
use Drupal\Core\File\FileSystemInterface;
$file = $file_system->copy($source, $destination, FileSystemInterface::EXISTS_REPLACE);NEW:
use Drupal\Core\File\FileExists;
$file = $file_system->copy($source, $destination, FileExists::Replace);Twig Deprecations
spaceless Filter
Deprecated in: Twig 3.12 Removed in: Drupal 11.0.0
OLD:
{% apply spaceless %}
<div>
<span>Content</span>
</div>
{% endapply %}NEW:
{# Remove spaceless - use CSS or HTML minification instead #}
<div>
<span>Content</span>
</div>Module Info File Changes
core_version_requirement
Required for: Drupal 11 compatibility
OLD:
name: My Module
type: module
core_version_requirement: ^9 || ^10NEW:
name: My Module
type: module
core_version_requirement: ^9 || ^10 || ^11Create patch:
cd docroot/modules/contrib/my_module
# Edit my_module.info.yml
git diff my_module.info.yml > ../../../patches/my_module-d11-info.patchQuick Reference: Service Names
| Old Function/Constant | Service Name | Interface |
|---|---|---|
| REQUEST_TIME | datetime.time | Drupal\Core\Datetime\TimeInterface |
| user_roles() | N/A | Drupal\user\Entity\Role::loadMultiple() |
| file_validate_extensions() | file.validator | Drupal\file\Validation\FileValidatorInterface |
| system_retrieve_file() | file_system + http_client | FileSystemInterface + ClientInterface |
| _drupal_flush_css_js() | asset.query_string | Drupal\Core\Asset\AssetQueryStringInterface |
Testing Your Fixes
After making changes, always:
1. Clear cache: drush cr 2. Run upgrade_status: drush upgrade_status:analyze module_name 3. Check logs: drush watchdog:show --severity=Error 4. Visit pages: Test actual functionality 5. Run tests: If module has tests, run them
Finding More Information
- Deprecation Policy: https://www.drupal.org/about/core/policies/core-change-policies/drupal-deprecation-policy
- Change Records: https://www.drupal.org/list-changes/drupal
- API Documentation: https://api.drupal.org/api/drupal/11.x
- Upgrade Status Module: https://www.drupal.org/project/upgrade_status
Drupal Patches: Complete Workflow Guide
Finding Patches in Issue Queues
Step 1: Navigate to Module Issue Queue
URL Pattern: https://www.drupal.org/project/issues/MODULE_NAME
Example: https://www.drupal.org/project/issues/audiofield
Step 2: Search for Your Issue
Filter Options:
- Status: Open, Needs review, Reviewed & tested by the community (RTBC)
- Category: Bug report, Task, Feature request, Support request
- Version: Match your module version
- Priority: Critical, Major, Normal, Minor
Search Tips:
- Use specific error messages in search
- Search for "Deprecated" or "Drupal 11" for compatibility issues
- Look for "[META]" issues that track multiple related problems
- Check "Needs tests" status - patches with tests are more reliable
Step 3: Evaluate the Issue
Good Signs: ✅ Status is "Reviewed & tested by the community" (RTBC) ✅ Automated tests are passing (green checkmark) ✅ Multiple people report it works ✅ Recent activity/comments ✅ Patch is against the version you're using ✅ Maintainer has reviewed/commented
Red Flags: ❌ Tests failing (red X) ❌ Old patch (1+ years) with no recent activity ❌ Comments saying "doesn't work" or "breaks X" ❌ Patch is for wrong version (e.g., 8.x patch for 9.x module) ❌ Multiple competing patches with no consensus
Step 4: Find the Patch File
Look for:
- Green "Interdiff" and "File" links in comments
- File attachments with
.patchextension - Most recent patch at bottom of issue
- Patch naming:
module-brief-description-NODEID-COMMENT.patch
Example:
audiofield-file-validator-3432063-12.patch
└─ module: audiofield
└─ description: file-validator
└─ node ID: 3432063
└─ comment number: 12Composer-Patches Plugin Workflow
Understanding the Plugin Commands
The cweagans/composer-patches plugin provides specific commands for managing patches:
`composer patches-relock`:
- Regenerates
patches.lock.jsonfromcomposer.jsondefinitions - Run after adding/removing/modifying patch definitions
- Similar to how
composer update --lockworks for dependencies
`composer patches-repatch`:
- Removes all patched dependencies and reinstalls them with current patches
- WARNING: This deletes dependency directories - commit changes first!
- Use after
patches-relockto apply new patches
`composer patches-doctor`:
- Diagnostic tool to identify configuration issues
- Run this first when patches fail
Proper Workflow for Adding Patches
Step 1: Define patch in composer.json
{
"extra": {
"patches": {
"drupal/module_name": {
"Description of fix": "patches/module-fix.patch"
}
}
}
}Step 2: Regenerate patches lock file
composer patches-relockStep 3: Apply patches
# WARNING: This removes and reinstalls dependencies!
# Commit or stash changes first
composer patches-repatchStep 4: Verify and commit
# Test that patches applied correctly
drush upgrade_status:analyze module_name
# Commit all three files
git add composer.json composer.lock patches.lock.json patches/
git commit -m "Add patch for module_name"Important Files
patches.lock.json:
- Locks patch definitions like
composer.locklocks versions - Generated by
composer patches-relock - Must be committed to version control
- When present, patches install from here (not composer.json)
Key Insight: Once patches.lock.json exists, it's the source of truth for patch application, not composer.json directly.
Applying Patches via Composer
Method 1: Remote Patch (from Drupal.org)
{
"extra": {
"patches": {
"drupal/audiofield": {
"Fix file_validate_extensions deprecation": "https://www.drupal.org/files/issues/2024-06-15/audiofield-file-validator-3432063-12.patch"
}
}
}
}Steps: 1. Right-click patch link → Copy link address 2. Add to composer.json patches section 3. Run composer install or composer update drupal/audiofield --with-all-dependencies
Method 2: Local Patch
{
"extra": {
"patches": {
"drupal/audiofield": {
"Custom fix for file validation": "patches/audiofield-custom-fix.patch"
}
}
}
}Directory Structure:
project-root/
├── patches/
│ ├── audiofield-custom-fix.patch
│ ├── entity_limit-user-roles-fix.patch
│ └── module-name-issue-description.patch
├── composer.json
└── docroot/Method 3: Merge Request Diff
For GitLab merge requests:
{
"extra": {
"patches": {
"drupal/social_auth_google": {
"Icon fix": "https://git.drupalcode.org/project/social_auth_google/-/merge_requests/4/diffs.patch"
}
}
}
}Format: https://git.drupalcode.org/project/MODULE/-/merge_requests/NUMBER/diffs.patch
Creating Your Own Patches
When to Create a Patch
1. No existing patch in issue queue 2. Existing patch is outdated and doesn't apply 3. Quick local fix while waiting for upstream 4. Custom modification specific to your project
Method 1: Git Diff (Recommended)
# Navigate to contrib module
cd docroot/modules/contrib/audiofield
# Make your changes to the files
# Edit src/AudioFieldPluginBase.php, etc.
# Create patch
git diff > /path/to/patches/audiofield-custom-fix.patch
# Or from project root:
cd /path/to/project
git diff docroot/modules/contrib/audiofield > patches/audiofield-custom-fix.patchAdvantages:
- Clean, standard format
- Preserves file paths correctly
- Works with composer-patches plugin
Creating Patches for Modules with Existing Patches
Three Scenarios:
1. Independent patches (different files or non-conflicting sections)
- Create patch against original source - patches apply in any order
- No special handling needed
2. Patches that need to stack (same file, but don't conflict)
- Create new patch against patched state
- Patches apply in order defined in composer.json
- Line numbers in new patch account for previous patches
3. Conflicting patches (overlapping changes)
- Best practice: Create a combined patch that replaces the conflicting patches
- Incorporates all changes from conflicting patches into one
- Simpler, more maintainable, more reliable
This section covers scenario 3: Creating combined patches when conflicts exist.
Why Combined Patches?
- ✅ Single source of truth for all changes
- ✅ No dependency on patch application order
- ✅ Easier to review and understand
- ✅ Eliminates stacking complexity
- ✅ More maintainable long-term
Solution: Create combined patch from the installed module directory.
# Step 1: Let composer install the module with all existing patches applied
composer install
# Step 2: Navigate to the installed contrib module (now has ALL patches applied)
cd docroot/modules/contrib/entity_limit
# Step 3: Initialize temporary git repo to track changes
git init
git add -A
git commit -m "After all existing patches"
# Step 4: Make your additional changes
# Edit src/Plugin/EntityLimit/UserLimit.php, etc.
# Step 5: Generate combined patch with --no-prefix flag
git diff --no-prefix > /path/to/patches/entity_limit-combined-fixes-d11.patch
# Step 6: Clean up temporary git repo
cd /path/to/project
rm -rf docroot/modules/contrib/entity_limit/.gitStep 7: Update composer.json
{
"extra": {
"patches": {
"drupal/entity_limit": {
// Remove or comment out the old conflicting patches:
// "checkAccess() throws an exception": "https://...",
// "Drupal calls should be avoided": "https://...",
// Add your combined patch that includes both fixes plus new changes:
"Combined D11 compatibility fixes": "patches/entity_limit-combined-fixes-d11.patch"
}
}
}
}Why combined patches are better:
- Single patch incorporates all changes (old patches + your new changes)
- Replaces multiple conflicting patches in composer.json
- No dependency on patch application order
- Easier to maintain and understand
Alternative Method (as described by user):
# Create temp directory
mkdir /tmp/patch-work
cd /tmp/patch-work
# Clone the module repo at the correct tag/version
git clone --branch 3.0.0-beta1 https://git.drupalcode.org/project/entity_limit.git
cd entity_limit
# Apply existing patches manually if needed
patch -p1 < /path/to/existing-patch-1.patch
patch -p1 < /path/to/existing-patch-2.patch
# Make your changes
git add -A
git commit -m "Apply fix"
# Generate patch
git format-patch -1 --no-prefix > /path/to/patches/entity_limit-new-fix.patch
# Clean up
cd /path/to/project
rm -rf /tmp/patch-workReal-World Example - Stacking Approach:
entity_limit had two existing patches: 1. https://www.drupal.org/files/.../entity_limit--use_getkey_in_access_check--3347700-5.patch 2. https://www.drupal.org/files/.../3432063-2.patch
When adding a third patch to fix user_roles():
- Patches modified different parts of the file (RoleLimit.php vs UserLimit.php)
- No conflicts, but same file context
- Used stacking approach: created patch after existing patches applied
- Result: Three patches stack cleanly in composer.json
When to use combined patch instead: If existing patches had also modified UserLimit.php and conflicted with the user_roles() fix, the better approach would be: 1. Create combined patch including all changes 2. Replace all three patches with one combined patch in composer.json 3. Simpler maintenance, no stacking complexity
Decision Guide:
| Scenario | Approach | Reasoning |
|---|---|---|
| Patches in different files | Independent patches | No conflicts possible |
| Patches in same file, different sections | Stack patches | Works fine, no conflicts |
| Patches modify overlapping lines | Combined patch | Eliminates conflicts |
| Many small patches to same area | Combined patch | Easier maintenance |
| Mix of upstream + local patches | Stack patches | Keep upstream patches separate for easier updates |
Common Mistakes to Avoid:
- ❌ Trying to stack patches that actually conflict (use combined patch instead)
- ❌ Creating too many small stacking patches (combine them!)
- ❌ Manually adjusting line numbers in patch files
- ❌ Creating combined patches when simple stacking would work fine
Pro tip: Combined patches are your friend when patches conflict. Don't try to make conflicting patches stack - merge them!
Method 2: Diff Command (Alternative)
# Create backup of original file
cp original.php original.php.bak
# Make changes to original.php
# Create patch
diff -Naur original.php.bak original.php > module-fix.patch
# For directories
diff -Naur original-module/ modified-module/ > module-fix.patchMethod 3: Export from Issue Queue
If you made changes and want to contribute back:
cd docroot/modules/contrib/module_name
# Ensure clean git state
git status
# Make your changes
# Create patch for issue queue
git diff > module-issue-brief-description-NODEID-XX.patchPatch Naming Convention
Format: module-brief-description-NODEID-COMMENT.patch
Examples:
audiofield-file-validator-3432063-12.patchentity_limit-user-roles-fix-3445678-2.patchlicensing-d11-compat-3456789-5.patch
Best Practices:
- Use lowercase, hyphens (not underscores)
- Keep description brief but descriptive
- Include node ID if contributing to d.o issue
- Increment comment number for revisions
Handling Dev Branches
When Fix is Committed but Not Released
Scenario: Issue is closed as "Fixed" but no new release yet.
Check the Status: 1. Go to module's Drupal.org project page 2. Click "Releases" tab 3. Check "Development release" section 4. Note the latest commit or branch
Option 1: Use Dev Version
# Switch to dev branch (e.g., 1.x-dev)
composer require drupal/module_name:1.x-dev --with-all-dependenciescomposer.json:
{
"require": {
"drupal/module_name": "1.x-dev"
}
}Warning: Dev versions are unstable - use cautiously in production
Option 2: Use Specific Commit
{
"require": {
"drupal/module_name": "dev-1.x#abc123def456"
}
}Replace abc123def456 with actual commit hash from GitLab.
Option 3: Wait for Release
If it's close to release, consider waiting and using temporary patch.
Verifying Patches During Module Upgrades
CRITICAL: When Removing Patches After Upgrade
The Problem: When upgrading a module, patches may fail to apply. It's tempting to simply remove non-applying patches from composer.json, but this can cause you to lose important customizations.
The Rule: BEFORE removing any patch, you MUST verify one of three things:
1. ✅ The patch was merged upstream - Changes are now in the module 2. ✅ A new patch exists - Updated patch in the issue queue for the new version 3. ✅ You can re-roll the patch - Create an updated patch for the new version
Never remove a patch without checking! If none of the above are true, you've just lost your customizations.
Step-by-Step Verification Process
Step 1: Identify which patches failed
When you upgrade and patches fail:
composer update drupal/module_name --with-all-dependencies
# Output shows:
# Cannot apply patch https://git.drupalcode.org/project/module/-/merge_requests/10.diff!
# Cannot apply patch patches/module-custom-fix.patch!Step 2: For each failed patch, check its status
For Merge Request patches:
# Visit the MR URL in a browser
# Example: https://git.drupalcode.org/project/social_auth_apple/-/merge_requests/10
# Check:
# - Is it merged? (Look for "Merged" badge)
# - What issue does it address? (Check the description)
# - What changes does it make? (View the diff)For Issue Queue patches:
# Visit the issue node
# Example: https://www.drupal.org/node/3432063
# Check:
# - Status: "Fixed" means merged, "Active" means not merged
# - Are there newer patches for your version?
# - Read recent comments for status updatesStep 3: Verify if changes are in the new version
Method 1: Check the actual code
# Read the file that the patch modified
cat docroot/modules/contrib/module/src/FileName.php | grep "specific_function_or_code"
# Download the patch to see what it changed
curl https://git.drupalcode.org/project/module/-/merge_requests/10.diff | head -50
# Compare: Does the current code include the patch's changes?Method 2: Check PATCHES.txt
# Some modules document applied patches
cat docroot/modules/contrib/module/PATCHES.txt
# This may list patches that were committedMethod 3: Compare with upstream
# Initialize git in the module directory
cd docroot/modules/contrib/module
git init
git add -A
git commit -m "Current version"
# Download the patch and try to apply it
curl -O https://path/to/patch.patch
patch -p1 --dry-run < patch.patch
# If it says "already applied", the changes are in!
# If it fails, read the error to see whyStep 4: Take appropriate action
Based on your findings:
Case A: Patch was merged upstream ✅
{
"patches": {
"drupal/module": {
// Remove this patch - it's now in the module
// "Fix from MR !10": "https://git.drupalcode.org/project/module/-/merge_requests/10.diff"
}
}
}No further action needed - your customization is preserved in the new version.
Case B: Patch not merged, but updated version exists ✅
{
"patches": {
"drupal/module": {
// Update to new patch for new version
"Fix from issue #123": "https://www.drupal.org/files/issues/2024-11-01/module-fix-123-15.patch"
}
}
}Case C: Patch not merged, no update exists - MUST RE-ROLL ⚠️
# Step 1: Install the new version (without the patch temporarily)
composer update drupal/module --with-all-dependencies
# Step 2: Navigate to the module
cd docroot/modules/contrib/module
# Step 3: Initialize git repo
git init
git add -A
git commit -m "Clean install of version X.Y.Z"
# Step 4: Recreate the patch's changes manually
# - Review the old patch to understand what it did
# - Make the same logical changes in the new code
# - The code may have moved or been refactored
# Step 5: Generate new patch
git diff --no-prefix > /path/to/patches/module-fix-rerolled-for-XY.patch
# Step 6: Clean up
rm -rf .git
cd /path/to/project
# Step 7: Update composer.json
{
"patches": {
"drupal/module": {
"Fix X (re-rolled for 2.x)": "patches/module-fix-rerolled-for-XY.patch"
}
}
}
# Step 8: Test the new patch
composer install
drush cr
# Test functionalityReal-World Example: social_auth 3.x → 4.x Upgrade
Situation: Upgrading from social_auth 3.x to 4.x, two patches failed to apply.
Patch 1: social_auth_google Icon (MR !4)
# Check MR status
# URL: https://git.drupalcode.org/project/social_auth_google/-/merge_requests/4
# Status: Open (not merged)
# Check if change is in 4.x
cat docroot/modules/contrib/social_auth_google/img/google_logo.svg | head -3
# Output: <svg version="1.1" xmlns="http://www.w3.org/2000/svg"...
# Compare with patch
curl -s https://git.drupalcode.org/project/social_auth_google/-/merge_requests/4/diffs.patch | grep -A 2 "^+"
# Output shows same SVG code!
# Conclusion: ✅ Patch was merged upstream
# Action: Remove patch from composer.json - no re-roll neededPatch 2: social_auth_apple Allow league settings alter (MR !10)
# Check MR status
# Status: Open (not merged)
# Read the new code
cat docroot/modules/contrib/social_auth_apple/src/Plugin/Network/AppleAuth.php
# The 4.x version has completely different architecture!
# Old: initSdk() was in module, patch added alter hook
# New: initSdk() is in parent class, module uses getExtraSdkSettings()
# Conclusion: ⚠️ Patch not merged, architecture changed - must re-roll
# Action: Re-implement the alter hook for new architectureRe-rolling the Apple patch:
# Init git repo
cd docroot/modules/contrib/social_auth_apple
git init && git add -A && git commit -m "Initial 2.0.1"
# Override initSdk() method to add alter hook (adapted for new architecture)
# Edit src/Plugin/Network/AppleAuth.php
# Add:
# protected function initSdk(): mixed {
# // Copy parent logic, add alter hook before instantiation
# $this->networkManager->getModuleHandler()->alter('social_auth_apple_settings', $league_settings, $this->settings);
# return new $network['class_name']($league_settings);
# }
# Generate patch
git diff --no-prefix > /path/to/patches/social_auth_apple-allow-league-settings-alter-2x.patch
# Clean up
rm -rf .git
# Update composer.json with new patchChecklist for Patch Verification
Use this checklist when removing patches after an upgrade:
- [ ] Identified all patches that failed to apply
- [ ] For each patch, determined what it fixes/adds
- [ ] Checked if MR/issue is merged upstream
- [ ] Verified if changes exist in the new version's code
- [ ] If not merged: Searched for updated patch in issue queue
- [ ] If no update: Re-rolled the patch for new version
- [ ] Tested that re-rolled patch applies cleanly
- [ ] Verified functionality still works
- [ ] Updated composer.json with new/removed patches
- [ ] Documented changes in commit message
Common Mistakes to Avoid
❌ Mistake 1: Removing patches without checking if they were merged
# Wrong approach:
# "Patch doesn't apply anymore, just remove it"
# Result: Lost customization✅ Correct: Check if the functionality is in the new version
# Read the patch to understand what it does
# Check the new code to see if those changes are present
# Only remove if confirmed upstream❌ Mistake 2: Assuming failed patch means it's no longer needed
# Wrong assumption:
# "Module was upgraded, probably fixed now"
# Result: Feature broken, users affected✅ Correct: Verify the specific functionality
# Test the feature the patch was enabling/fixing
# If still broken, re-roll the patch❌ Mistake 3: Re-rolling patch without understanding architecture changes
# Wrong approach:
# "Just make the patch apply to the new file"
# Result: Patch applies but doesn't work✅ Correct: Understand how the new version works
# Read both old and new code
# Understand what changed architecturally
# Adapt the patch logic to new architectureTesting Patches
Before Applying
# Download patch
curl -O https://www.drupal.org/files/issues/2024-01-15/module-fix-1234567-8.patch
# Preview what will change
patch -p1 --dry-run < module-fix-1234567-8.patch
# Check if it applies cleanly
cd docroot/modules/contrib/module_name
git apply --check /path/to/patch.patchAfter Applying
# Clear cache
drush cr
# Run database updates if needed
drush updb -y
# Check for errors
drush watchdog:show --severity=Error --count=20
# Test functionality
# Visit pages that use the module
# Perform actions affected by the patch
# Run module's tests if available
cd docroot
../vendor/bin/phpunit modules/contrib/module_name/tests/Verify with Upgrade Status
# Re-scan module to confirm fix
drush upgrade_status:analyze module_name
# Should show issue as resolvedCommon Patch Scenarios
Scenario 1: Patch Fails to Apply
Error: "patch ... failed at line X"
Solutions:
1. Check module version:
composer show drupal/module_name
# Ensure patch matches your version2. Look for updated patch:
- Go to issue node:
drupal.org/node/NODEID - Read recent comments for newer patch
- Update composer.json with new patch URL
3. Rebase patch manually:
cd docroot/modules/contrib/module_name
# Apply what works
patch -p1 < /path/to/patch.patch
# Manually fix conflicts
# Create new patch
git diff > /path/to/patches/module-rebased.patchScenario 2: Multiple Patches for Same Module
composer.json:
{
"extra": {
"patches": {
"drupal/entity_limit": {
"Fix 1: Access check exception": "https://www.drupal.org/files/issues/2023-09-24/entity_limit-3347700-5.patch",
"Fix 2: Drupal calls removed": "https://www.drupal.org/files/issues/2024-03-19/3432063-2.patch",
"Fix 3: D11 info.yml": "patches/entity_limit-d11-info.patch"
}
}
}
}Order Matters: Patches apply in order listed. Ensure they don't conflict.
Scenario 3: Patch Already Applied
Error: "Skipping patch ... (already applied)"
Cause: Module maintainer merged the patch
Solution: Remove patch from composer.json
# Edit composer.json - remove patch entry
# Reinstall
composer installScenario 4: Understanding Patched vs Unpatched State
CRITICAL: Before creating or applying patches, understand the current state of files on disk.
Check if files are already patched:
# List patches applied to a module
composer show drupal/module_name
# Check the PATCHES.txt file (if it exists)
cat docroot/modules/contrib/module_name/PATCHES.txt
# Review composer.json to see what should be applied
grep -A 5 "drupal/module_name" composer.jsonVerify actual file state:
# Read the actual code
cat docroot/modules/contrib/module_name/src/SomeFile.php | grep -A 5 "deprecated_function"
# Compare with original from drupal.org
curl -s https://ftp.drupal.org/files/projects/module_name-VERSION.tar.gz | tar xzO module_name/src/SomeFile.php | grep -A 5 "deprecated_function"When adding a new patch to a module with existing patches:
1. Apply patches incrementally to understand dependencies:
# Temporarily comment out all but first patch
# Run: composer install
# Check what changed
# Add second patch, reinstall, check again
# Continue until you find the conflict2. Check if existing patches already fix your issue:
# Download and read existing patch
curl https://www.drupal.org/files/issues/YYYY-MM-DD/module-fix-NODEID-X.patch
# Look for your function name
grep "user_roles\|system_retrieve_file\|_drupal_flush" downloaded.patch3. If conflict exists, create combined patch:
cd docroot/modules/contrib/module_name
# Ensure module is in clean patched state (existing patches applied)
composer install
# Make your additional changes
# Edit files as needed
# Create combined patch that includes your changes ON TOP of existing patches
git diff > ../../../patches/module-combined-fixes.patch
# Update composer.json: remove conflicting individual patches, add combined oneScenario 5: Debugging Patch Application Failures
Systematic approach:
# Step 1: Check module version matches patch
composer show drupal/module_name | grep versions
# Step 2: Try applying patch manually to see exact error
cd docroot/modules/contrib/module_name
curl -O https://www.drupal.org/files/issues/.../patch.patch
patch -p1 --dry-run < patch.patch
# Read the error carefully - which file? which line?
# Step 3: Inspect the file that's failing
cat src/FailingFile.php | head -100
# Is this file already modified by another patch?
# Step 4: Check patch order in composer.json
# Patches apply in the order listed
# Earlier patches may modify context for later patches
# Step 5: Apply patches one by one
# Remove all patches from composer.json except first
# composer install
# Add second patch, composer install
# Continue until failure occurs
# Now you know which two patches conflictResolution strategies:
1. Patches complement each other (modify different files):
- Keep both patches, order doesn't matter
2. Patches modify same file, different sections:
- Try reversing order in composer.json
- If still fails, create combined patch
3. Patches modify overlapping code:
- Must create combined patch
- Apply first patch, then manually apply second patch changes, create new patch from result
Scenario 6: Creating Patch for Deprecation
Example: Replace user_roles() in licensing module
cd docroot/modules/contrib/licensing
# Edit src/Form/LicenseTypeForm.php
# Replace user_roles() with Role::loadMultiple() pattern
# Create patch
git diff > ../../../patches/licensing-user-roles-d11-fix.patch
# Verify patch format
cat ../../../patches/licensing-user-roles-d11-fix.patchAdd to composer.json:
{
"extra": {
"patches": {
"drupal/licensing": {
"Replace deprecated user_roles() for D11": "patches/licensing-user-roles-d11-fix.patch",
"Drupal 11 .info.yml support": "patches/licensing-d11-info.patch"
}
}
}
}Lessons Learned: Real-World Patch Conflicts
Case Study: entity_limit user_roles() Fix
Initial Situation:
- Module had 3 existing patches applied
- Needed to add fix for user_roles() deprecation
- New patch failed to apply: "Cannot apply patch!"
Root Cause:
- Existing patch (#3432063-2) had already modified RoleLimit.php
- New patch tried to modify same lines
- Patch context didn't match because file was already in patched state
Wrong Approach ❌:
- Edit files directly without understanding existing patches
- Try to create patch from scratch against original module
Right Approach ✅: 1. Check which files existing patches modify:
curl https://www.drupal.org/files/issues/2024-03-19/3432063-2.patch | grep "^diff"
curl https://www.drupal.org/files/issues/2024-03-19/3432063-2.patch | grep "user_roles"2. Discovered existing patch already fixed RoleLimit.php!
3. Only needed to fix UserLimit.php (not touched by existing patches)
4. Edit UserLimit.php directly after ensuring composer patches are applied
5. No new patch needed - direct file edit works because it doesn't conflict
Key Takeaway: Always read existing patches before creating new ones. They may already include your fix.
Case Study: When to Create Combined Patches
Scenario: Need to fix 3 issues in same module:
- Issue A: Fixed by remote patch (patch-A.patch)
- Issue B: Fixed by remote patch (patch-B.patch)
- Issue C: No patch exists, need custom fix
If patch-A and patch-B modify same file:
Option 1: Try applying sequentially
{
"patches": {
"drupal/module": {
"Fix A": "https://drupal.org/files/patch-A.patch",
"Fix B": "https://drupal.org/files/patch-B.patch",
"Fix C": "patches/custom-fix-C.patch"
}
}
}If this fails:
Option 2: Create combined local patch
# Apply patch A
composer require drupal/module
# Manually apply patch B changes
# Add your fix C changes
# Create combined patch
git diff > patches/module-combined-A-B-C.patch{
"patches": {
"drupal/module": {
"Combined fixes for A, B, and C": "patches/module-combined-A-B-C.patch"
}
}
}Document in patch what it includes:
Combined patch for drupal/module includes:
- Fix A from drupal.org/node/XXXXX (patch-A.patch)
- Fix B from drupal.org/node/YYYYY (patch-B.patch)
- Custom fix C for issue described herePatch Management Best Practices
Organization
patches/
├── contrib/ # Patches for contrib modules
│ ├── audiofield-file-validator-fix.patch
│ └── entity_limit-user-roles-fix.patch
├── core/ # Patches for Drupal core
│ └── core-fix-something-123456-7.patch
└── custom/ # Patches for custom code (rare)Alternative: Keep all in patches/ with descriptive names
Documentation
Add comments in composer.json:
{
"extra": {
"patches": {
"drupal/audiofield": {
"Fix file_validate_extensions deprecation (D11) - See drupal.org/node/3432063": "patches/audiofield-file-validator-3432063-12.patch"
}
}
}
}Version Control
Always commit:
composer.jsonchanges- Patch files in
patches/directory composer.lockafter applying
Ignore:
- Modified contrib module files (patches handle changes)
- Temporary patch files
.gitignore:
docroot/modules/contrib/*/
!patches/Updating Patches
When module updates, patches may need updating:
# Update module
composer require drupal/module_name:^2.0 --with-all-dependencies
# If patch fails:
# 1. Check if fix is in new version (remove patch)
# 2. Find updated patch in issue queue
# 3. Rebase patch manually if needed
# Test after reapplying
drush cr
drush updb -yContributing Patches Back
Create Issue-Ready Patch
cd docroot/modules/contrib/module_name
# Create patch with proper format
git diff > /tmp/module-issue-brief-description-NODEID-XX.patch
# Test patch applies cleanly
git apply --reverse /tmp/module-issue-brief-description-NODEID-XX.patch
git apply /tmp/module-issue-brief-description-NODEID-XX.patchUpload to Issue Queue
1. Comment on issue: Explain your changes 2. Upload patch: Use "File" button 3. Set status: Usually "Needs review" 4. Provide test results: Describe testing performed 5. Tag appropriately: Add version tags
Interdiff for Revisions
When updating someone else's patch:
# Download previous patch
curl -O https://www.drupal.org/files/issues/2024-01-15/module-fix-NODEID-10.patch
# Create your new patch
git diff > module-fix-NODEID-12.patch
# Create interdiff showing changes between patches
interdiff module-fix-NODEID-10.patch module-fix-NODEID-12.patch > NODEID-10-12-interdiff.txt
# Upload both: new patch AND interdiffQuick Reference
Essential Commands
# Apply patch manually
patch -p1 < patch-file.patch
# Reverse patch
patch -p1 -R < patch-file.patch
# Create patch from git
git diff > patch-file.patch
# Test if patch applies
git apply --check patch-file.patch
# View patch contents
cat patch-file.patch
# Apply with composer
composer install
composer update drupal/module_name --with-all-dependenciesCommon Patch Locations
- Issue queue:
drupal.org/project/issues/MODULE_NAME - Module releases:
drupal.org/project/MODULE_NAME/releases - Git commits:
git.drupalcode.org/project/MODULE_NAME - Merge requests:
git.drupalcode.org/project/MODULE_NAME/-/merge_requests
Troubleshooting Quick Fixes
| Problem | Solution |
|---|---|
| Patch won't apply | Check module version, find updated patch |
| Patch already applied | Remove from composer.json |
| Wrong path in patch | Edit patch file or use -pX flag |
| Conflicts after update | Rebase patch or check if fix is included |
| Tests failing | May not be patch issue - check logs |
Resources
- Composer Patches Plugin: https://github.com/cweagans/composer-patches
- Drupal Patch Naming: https://www.drupal.org/node/1054616
- Creating Patches: https://www.drupal.org/node/707484
- Git for Patches: https://www.drupal.org/node/2135321
- Issue Queue Guide: https://www.drupal.org/issue-queue
Drupal Issue Queue RSS Feeds
Quick Access to Issue Information
Issue queues provide RSS feeds for automated monitoring and searching.
RSS Feed URL Pattern
Base Pattern:
https://www.drupal.org/project/issues/rss/MODULE_NAME?paramsExamples:
https://www.drupal.org/project/issues/rss/audiofield
https://www.drupal.org/project/issues/rss/entity_limit
https://www.drupal.org/project/issues/rss/viewsQuery Parameters
Complete Parameter Set
https://www.drupal.org/project/issues/rss/MODULE_NAME?text=SEARCH&status=STATUS&priorities=PRIORITY&categories=CATEGORY&version=VERSION&component=COMPONENTParameter Options
text: Free-text search
- Example:
text=deprecated - Example:
text=Drupal+11
status: Issue status filter
Open- Active issuesFixed- Resolved issuesClosed- Closed issuesActive- Open or needs reviewNeeds+review- Waiting for reviewNeeds+work- Needs additional workReviewed+%26+tested+by+the+community- RTBC (ready to commit)All- All statuses
priorities: Priority level
1- Critical2- Major3- Normal4- MinorAll- All priorities
categories: Issue type
1- Bug report2- Task3- Feature request4- Support request5- PlanAll- All categories
version: Module version
- Example:
8.x-1.x - Example:
2.0.x All- All versions
component: Module component (if applicable)
- Varies by module
All- All components
Practical Examples
Find Drupal 11 Compatibility Issues
https://www.drupal.org/project/issues/rss/audiofield?text=Drupal+11&status=All&priorities=All&categories=All&version=All&component=AllFind Active Deprecation Issues
https://www.drupal.org/project/issues/rss/entity_limit?text=deprecated&status=Open&priorities=All&categories=1&version=All&component=AllFind RTBC (Ready to Commit) Issues
https://www.drupal.org/project/issues/rss/licensing?text=&status=Reviewed+%26+tested+by+the+community&priorities=All&categories=All&version=All&component=AllFind Recently Fixed Issues
https://www.drupal.org/project/issues/rss/social_auth?text=&status=Fixed&priorities=All&categories=All&version=All&component=AllUsing RSS Feeds Programmatically
Fetch with curl
# Fetch issues as XML
curl "https://www.drupal.org/project/issues/rss/audiofield?text=deprecated&status=All" > audiofield-issues.xml
# Parse with grep for quick search
curl -s "https://www.drupal.org/project/issues/rss/audiofield?text=Drupal+11" | grep -o '<title>.*</title>' | sed 's/<[^>]*>//g'Fetch with WebFetch (Claude Code)
// Use WebFetch tool to analyze RSS feed
const url = "https://www.drupal.org/project/issues/rss/audiofield?text=Drupal+11&status=All";
const prompt = "List all issues related to Drupal 11 compatibility with their status";Parse with xmllint
# Get issue titles and links
curl -s "https://www.drupal.org/project/issues/rss/audiofield?text=deprecated" | \
xmllint --xpath "//item/title/text()" -
# Get issue descriptions
curl -s "https://www.drupal.org/project/issues/rss/audiofield" | \
xmllint --xpath "//item/description/text()" -RSS Feed Structure
RSS feeds contain:
item: Each issue
- title: Issue title
- link: Issue URL (drupal.org/node/NODEID)
- description: Issue description/summary
- pubDate: Publication date
- dc:creator: Issue creator
- guid: Unique identifier
Example XML:
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>audiofield issues</title>
<item>
<title>Replace deprecated file_validate_extensions()</title>
<link>https://www.drupal.org/node/3432063</link>
<description>The function file_validate_extensions() is deprecated...</description>
<pubDate>Wed, 15 Jun 2024 14:23:00 +0000</pubDate>
<dc:creator>username</dc:creator>
<guid>https://www.drupal.org/node/3432063</guid>
</item>
</channel>
</rss>Common Search Patterns
Finding Patches for Your Version
# Search for issues on specific version
MODULE="audiofield"
VERSION="8.x-1.x"
curl "https://www.drupal.org/project/issues/rss/${MODULE}?version=${VERSION}&status=Active"Monitor Critical Issues
# Get critical bugs
MODULE="entity_limit"
curl "https://www.drupal.org/project/issues/rss/${MODULE}?priorities=1&categories=1&status=Open"Find Deprecated Function Issues
# Search for specific deprecated function
FUNCTION="user_roles"
MODULE="licensing"
curl "https://www.drupal.org/project/issues/rss/${MODULE}?text=${FUNCTION}"Automated Monitoring Script
#!/bin/bash
# monitor-module-issues.sh
# Check for new Drupal 11 issues
MODULES=("audiofield" "entity_limit" "licensing" "social_auth")
SEARCH="Drupal+11"
for MODULE in "${MODULES[@]}"; do
echo "Checking $MODULE for Drupal 11 issues..."
RSS_URL="https://www.drupal.org/project/issues/rss/${MODULE}?text=${SEARCH}&status=Active"
# Fetch and display issue titles
curl -s "$RSS_URL" | \
grep -o '<title>.*</title>' | \
sed 's/<[^>]*>//g' | \
tail -n +2 # Skip channel title
echo "---"
doneTips for Effective RSS Usage
1. Bookmark specific searches: Save frequently used RSS URLs 2. Use RSS readers: Feedly, Inoreader for monitoring 3. Automate checks: Cron jobs to check for new issues 4. Filter by date: Add &created= parameter for recent issues 5. Combine with curl: Quick command-line checks
Limitations
- RSS feeds show limited results (typically 25-50 latest)
- No advanced filtering (e.g., AND/OR logic)
- Some metadata not included in RSS
- For comprehensive search, use web interface
Web Interface URLs
Standard URL:
https://www.drupal.org/project/issues/MODULE_NAME?text=&status=All&priorities=All&categories=All&version=All&component=All&page=1Parameters match RSS but with pagination:
page=0- First pagepage=1- Second page- etc.
Quick Reference Commands
# Get all open issues
curl "https://www.drupal.org/project/issues/rss/MODULE?status=Open"
# Get deprecated function issues
curl "https://www.drupal.org/project/issues/rss/MODULE?text=deprecated"
# Get D11 compatibility
curl "https://www.drupal.org/project/issues/rss/MODULE?text=Drupal+11"
# Get RTBC issues (ready for patches)
curl "https://www.drupal.org/project/issues/rss/MODULE?status=Reviewed+%26+tested"
# Count issues
curl -s "https://www.drupal.org/project/issues/rss/MODULE" | grep -c "<item>"Integration with Workflow
When searching for patches:
1. Start with RSS feed for quick overview 2. Use text search for specific deprecations 3. Filter by status to find RTBC patches 4. Visit web interface for detailed patch files 5. Download patches from individual issue nodes
Example workflow:
# 1. Find issues
curl "https://www.drupal.org/project/issues/rss/audiofield?text=file_validate_extensions" | \
grep -o '<link>.*</link>' | sed 's/<[^>]*>//g'
# Output: https://www.drupal.org/node/3432063
# 2. Visit issue page to find patches
# 3. Apply patch via composer (see drupal-patches-workflow.md)Related skills
FAQ
Is Drupal Contrib Mgmt safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.