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

  1. Install Drupal 11.4.6 and drupal/canvas 1.10.1 with no patches.
  2. composer require drupal/canvas_override and enable it.
  3. Load any page. Confirmed on a stock Drupal 11 site: /, /user/login, /admin/structure/types, /admin/modules and /node/1/canvas all return the fatal above, and drush status terminates 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:

  • ComponentTreeLoader implements no interface.
  • Its service ID is its own FQCN, so a replacement is resolved by class.
  • Four consumers typehint the concrete class: ComponentTreeInputExtractor, ClientDataToEntityConverter, ContentTemplate and ApiLayoutController.

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:

  • 60c285f skipped the service swap when Canvas ships the class final, 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.
  • c56b7b8 gave each overridden node a backing canvas_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.

  1. 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.
  2. 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.
  3. Make ComponentTreeLoader extensible 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:

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

  • CanvasOverridePageResolver is removed.
  • CanvasOverrideServiceProvider::isComponentTreeLoaderExtendable() and CanvasOverrideServiceProvider::SWAPPED_PARAMETER are removed.
  • The canvas_page_insert, canvas_page_update and node_delete hook implementations are removed.

Data model changes

  • None. The canvas_override.node_pages key-value collection is no longer written; it was never part of config or schema.

Release notes snippet

  • Fail safely when Canvas ships ComponentTreeLoader as 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.
Command icon 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:

Comments

rajab natshah created an issue. See original summary.

rajab natshah’s picture

Issue summary: View changes

rajab natshah’s picture

Title: Installing the module on an unpatched Canvas fatals: cannot extend final class ComponentTreeLoader » Swap the ComponentTreeLoader without fataling on an unpatched Canvas
Issue summary: View changes

  • rajab natshah committed 60c285f5 on 1.0.x
    fix: #3620603 Skip the ComponentTreeLoader swap when Canvas ships it...
rajab natshah’s picture

Title: Swap the ComponentTreeLoader without fataling on an unpatched Canvas » Make per-content Canvas layouts work without patching Canvas
Issue summary: View changes

  • rajab natshah committed c56b7b8a on 1.0.x
    feat: #3620603 Edit per-content layouts on a backing canvas_page when...

rajab natshah’s picture

Title: Make per-content Canvas layouts work without patching Canvas » Remove the Canvas patch requirement via the module's own ComponentTreeLoader subclass
Category: Bug report » Task
Issue summary: View changes

Rewrote 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.

  • rajab natshah committed ba2cdb10 on 1.0.x
    revert: #3620603 Restore the ComponentTreeLoader subclass and...

  • rajab natshah committed f98017d4 on 1.0.x
    fix: #3620603 Fail safely when Canvas ships ComponentTreeLoader as final
    
rajab natshah’s picture

Title: Remove the Canvas patch requirement via the module's own ComponentTreeLoader subclass » Fail safely when Canvas ships ComponentTreeLoader as final, instead of fataling every page
Issue summary: View changes
rajab natshah’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.