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:
- Right-click patch link → Copy link address
- Add to composer.json patches section
- Run
composer patches-relock, thencomposer reinstall drupal/audiofield(a plaincomposer installdoes not re-patch an already-installed package)
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
- No existing patch in issue queue
- Existing patch is outdated and doesn't apply
- Quick local fix while waiting for upstream
- Custom modification specific to your project
Method 1: Git Diff (Recommended)
# Navigate to contrib module (web/modules/contrib/audiofield on web/ layouts)
cd docroot/modules/contrib/audiofield
# Composer-installed contrib is not a git repo: take a temporary baseline
git init -q && git add -A && git commit -qm pristine
# Make your changes to the files
# Edit src/AudioFieldPluginBase.php, etc.
# Create patch (paths relative to the module root, as composer-patches expects)
git diff > ../../../../patches/audiofield-custom-fix.patch
git checkout -- . && rm -rf .gitIf your project commits contrib code, git diff --relative run inside the module directory gives the same module-relative paths; a plain git diff docroot/modules/contrib/audiofield from the project root does NOT (its paths start with docroot/... and will not apply to the package).
Advantages:
- Clean, standard format
- Preserves file paths correctly
- Works with composer-patches plugin
Creating Patches for Modules with Existing Patches
Three Scenarios:
Independent patches (different files or non-conflicting sections)
- Create patch against original source - patches apply in any order
- No special handling needed
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
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:
https://www.drupal.org/files/.../entity_limit--use_getkey_in_access_check--3347700-5.patchhttps://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:
- Create combined patch including all changes
- Replace all three patches with one combined patch in composer.json
- 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:
- Go to module's Drupal.org project page
- Click "Releases" tab
- Check "Development release" section
- 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:
- ✅ The patch was merged upstream - Changes are now in the module
- ✅ A new patch exists - Updated patch in the issue queue for the new version
- ✅ 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: Relock and apply the new patch, then test
composer patches-relock
composer reinstall drupal/module
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
# Run database updates if needed (updatedb rebuilds caches when it finishes)
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:
- Check module version:
composer show drupal/module_name
# Ensure patch matches your versionLook for updated patch:
- Go to issue node:
drupal.org/node/NODEID - Read recent comments for newer patch
- Update composer.json with new patch URL
- Go to issue node:
Rebase patch manually:
cd docroot/modules/contrib/module_name
# Contrib is not a git repo: take a temporary baseline first
git init -q && git add -A && git commit -qm pristine
# Apply what works
patch -p1 < /path/to/patch.patch
# Manually fix conflicts
# Create new patch
git diff > /path/to/patches/module-rebased.patch
rm -rf .gitScenario 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, then relock and reinstall
composer patches-relock
composer reinstall drupal/module_nameScenario 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:
- Apply patches incrementally to understand dependencies:
# Temporarily remove all but the first patch from composer.json
# Run: composer patches-relock && composer reinstall drupal/module_name
# Check what changed
# Add second patch, relock + reinstall, check again
# Continue until you find the conflict- 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.patch- If conflict exists, create combined patch:
# Ensure module is in clean patched state (existing patches applied)
composer install
cd docroot/modules/contrib/module_name # or web/modules/contrib/module_name
# Contrib is not a git repo: take a temporary baseline of the patched state
git init -q && git add -A && git commit -qm "After existing patches"
# 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
rm -rf .git
# 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 patches-relock && composer reinstall drupal/module_name
# Add second patch, relock + reinstall again
# Continue until failure occurs
# Now you know which two patches conflictResolution strategies:
Patches complement each other (modify different files):
- Keep both patches, order doesn't matter
Patches modify same file, different sections:
- Try reversing order in composer.json
- If still fails, create combined patch
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 # or web/modules/contrib/licensing
# Contrib is not a git repo: take a temporary baseline first
git init -q && git add -A && git commit -qm pristine
# Edit src/Form/LicenseTypeForm.php
# Replace user_roles() with Role::loadMultiple() pattern
# Create patch
git diff > ../../../../patches/licensing-user-roles-d11-fix.patch
rm -rf .git
# 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 ✅:
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"Discovered existing patch already fixed RoleLimit.php!
Only needed to fix UserLimit.php (not touched by existing patches)
Edit UserLimit.php directly after ensuring composer patches are applied
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 (updatedb rebuilds caches when it finishes)
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
- Comment on issue: Explain your changes
- Upload patch: Use "File" button
- Set status: Usually "Needs review"
- Provide test results: Describe testing performed
- 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 (after editing extra.patches)
composer patches-relock
composer reinstall drupal/module_nameCommon 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