Problem/Motivation
Canvas Override replaces Canvas's ComponentTreeLoader with its own subclass, CanvasOverrideComponentTreeLoader, so the Canvas editor can operate on a node instead of only a canvas_page. Canvas ships that class as final, so the subclass only loads on a site that has patched it. Vardot projects get the patch through vardot/varbase-patches; everyone else does not, and the module has never declared the dependency.
On a stock, unpatched Canvas the result is a site-wide PHP fatal on the first request that instantiates the loader:
Fatal error: Class Drupal\canvas_override\Storage\CanvasOverrideComponentTreeLoader cannot extend final class Drupal\canvas\Storage\ComponentTreeLoader in modules/contrib/canvas_override/src/Storage/CanvasOverrideComponentTreeLoader.php on line 29
Every page fatals, /user/login and /admin/modules included, and drush cannot bootstrap, so the module cannot be switched off through the UI.
The scope of this issue is to remove the need for that patch. It is not to build a second editing mode.
Steps to reproduce
- Install Drupal 11.4.6 and
drupal/canvas1.10.1 with no patches. composer require drupal/canvas_overrideand enable it.- Load any page. Confirmed on a stock Drupal 11 site:
/,/user/login,/admin/structure/types,/admin/modulesand/node/1/canvasall return the fatal above, anddrush statusterminates abnormally.
For contrast, on Varbase 11 (varbase_project:11.0.x-dev, core 11.4.6, canvas 1.10.1, varbase-patches 11.0.x-dev) the patch is applied, ComponentTreeLoader is not final, the swap resolves to CanvasOverrideComponentTreeLoader, and /node/N/canvas opens the per-node editor normally.
Proposed resolution
Keep the subclass plus the service swap. It is the correct design, and there is no in-module alternative:
ComponentTreeLoaderimplements no interface.- Its service ID is its own FQCN, so a replacement is resolved by class.
- Four consumers typehint the concrete class:
ComponentTreeInputExtractor,ClientDataToEntityConverter,ContentTemplateandApiLayoutController.
Any replacement must therefore satisfy instanceof ComponentTreeLoader, which means it must extend it, which final forbids. Decoration fails those typehints; re-registering the service ID fails them too. (The module's existing same-service-ID swap of ComponentTreeMeetsRequirementsConstraintValidator works only because nothing typehints that concrete class.)
So the fix belongs upstream, in three lines: drop final, and promote two constructor properties to protected readonly so a subclass can reach them.
This issue is blocked on Canvas #3567225 (MR !948, currently closed). The case for reopening it, including the analysis above, is posted there.
Approach tried and rejected
Two commits landed on 1.0.x attempting to work around final from inside the module, and both are reverted in MR !17:
60c285fskipped the service swap when Canvas ships the classfinal, and disabled the enable checkbox with a warning. It avoided the fatal but left the feature unusable on every stock Canvas, with a checkbox that could not be ticked.c56b7b8gave each overridden node a backingcanvas_page, edited that natively, and copied the tree back onto the node on save. MR !16 refined its seeding.
The page-backed approach works — verified on a stock Canvas: the backing page seeded from the bundle's default full-content layout with entity-bound props evaluated against the node, the editor opened on it, publishing synced the tree back to the node field, and the front end rendered it. It was rejected anyway: it is a second editing mode with its own storage, its own one-way sync and its own failure modes, carried solely to avoid three lines upstream. MR !16 is closed unmerged.
Why this direction
Three routes to removing the patch requirement were tried. All are closed.
- Skip the service swap when the class is final (!14, merged then reverted). It avoided the fatal, but left the feature unusable on every stock Canvas with a checkbox that could not be ticked.
- Edit per-content layouts on a backing
canvas_page(!15, with seeding in !16, merged then reverted). It worked, and was rejected anyway: a second editing mode with its own storage, its own one-way sync and its own failure modes, carried solely to avoid three lines upstream. - Make
ComponentTreeLoaderextensible upstream (canvas!948). Closed by the Canvas maintainer on 10 August 2026: per-node component trees are a planned first-class feature, and swapping the loader would introduce an accidental API to something never meant to be extensible.
Copying the class into this module instead of subclassing it was also tried, and fails for the same reason a replacement service fails: Canvas's own consumers typehint the concrete class, so anything that is not that class throws a TypeError.
Verified against Canvas 1.10.1: ComponentTreeLoader is still final, and both promoted constructor properties are private readonly, so a subclass could not reach them even if final were dropped.
So the patch requirement cannot be removed from inside this module. What was left was the real defect underneath it: an unpatched Canvas took the whole site down rather than disabling one feature. This issue fixes that in !18, and the patch requirement is tracked upstream in #3567225.
Remaining tasks
- ✅ File an issue
- ✅ Addition/Change/Update/Fix
- ✅ Testing to ensure no regression
- ➖ Automated unit/functional testing coverage
- ➖ Developer Documentation support
- ➖ User Guide Documentation support
- ➖ UX/UI designer responsibilities
- ➖ Accessibility and Readability
- ❌ Reviewed by a human
- ❌ Code review by maintainers
- ❌ Full testing and approval
- ❌ Credit contributors
- ❌ Review with the product owner
- ❌ Update Release Notes
- ❌ Release
Blocked, outside this queue:
- ❌ Canvas #3567225 lands the three-line change
User interface changes
- The "Per-content Canvas layout editing is unavailable" warning and the disabled enable checkbox are removed from the content type form.
- The Canvas Override tab always opens the per-node editor; it no longer redirects to a backing
canvas_page.
API changes
CanvasOverridePageResolveris removed.CanvasOverrideServiceProvider::isComponentTreeLoaderExtendable()andCanvasOverrideServiceProvider::SWAPPED_PARAMETERare removed.- The
canvas_page_insert,canvas_page_updateandnode_deletehook implementations are removed.
Data model changes
- None. The
canvas_override.node_pageskey-value collection is no longer written; it was never part of config or schema.
Release notes snippet
- Fail safely when Canvas ships
ComponentTreeLoaderas a final class. An unpatched Canvas no longer fatals every page; the content type form explains why per-content layout editing is unavailable and disables the setting. Per-content layouts still need the Canvas patch listed on the project page.
Issue fork canvas_override-3620603
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 3620603-fail-safe-unpatched-canvas
changes, plain diff MR !18
- 3620603-revert-to-subclass
changes, plain diff MR !17
- 3620603-page-backed
changes, plain diff MR !15
- 3620603-fail-safely
changes, plain diff MR !14
- 3620603-installing-the-module
compare
- 3620603-no-seed
changes, plain diff MR !16
Comments
Comment #2
rajab natshahComment #4
rajab natshahComment #6
rajab natshahComment #11
rajab natshahRewrote the issue summary around the actual scope: remove the Canvas patch requirement via the module's own ComponentTreeLoader subclass. Records why no in-module workaround exists (no interface, FQCN service ID, four concrete typehints), that this is blocked on Canvas #3567225, and that the page-backed approach in MR !16 was tried, verified working, and rejected as a second editing mode. Retitled and recategorised from Bug report to Task.
Comment #15
rajab natshahComment #16
rajab natshah✅ Released canvas_override-1.0.0-rc1