Problem/Motivation

Since the Content templates 1.3.0 conversion to OOP hooks, the Quick Node Clone form submit callback is registered as an object callback using [$this, 'quickNodeCloneSubmit'].

This issue was introduced by #3596709: Automated Drupal 12 compatibility fixes for content_templates 1.0.x-dev

On /template/{node}/quick_clone, AJAX interactions that rebuild and cache the form, such as removing an image from a media field, cause the form array to be serialized. Because the #submit callback contains the hooks service object, serialization also attempts to serialize the service dependency graph, which includes an indirect reference to the database connection.

This results in the following exception during form cache storage:

LogicException: The database connection is not serializable.

This is a regression from the previous procedural hook implementation, where the submit callback was registered as a serializable function name. The issue breaks AJAX rebuilds on the clone form and prevents users from completing normal form interactions involving AJAX-enabled widgets.

Steps to reproduce

  1. Install Content templates 1.3.0 with Drupal core 11.3 or later.
  2. Install and enable the Quick Node Clone integration.
  3. Use a content type that has a media image field and can be cloned from a template.
  4. Open /template/{node}/quick_clone.
  5. In a media image field, click Remove.
  6. Observe that the AJAX request fails with LogicException: The database connection is not serializable instead of rebuilding the widget.

Proposed resolution

Register the submit callback as a serializable static callback instead of an object callback.

The patch changes the callback registration from:

array_unshift($form['actions']['submit']['#submit'], [$this, 'quickNodeCloneSubmit']);

to:

array_unshift($form['actions']['submit']['#submit'], [self::class, 'quickNodeCloneSubmit']);

It also changes quickNodeCloneSubmit() from an instance method to a static method:

public static function quickNodeCloneSubmit($form, FormStateInterface $form_state)

This resolves the issue because the form array now contains an array of serializable strings rather than the hooks service object. The callback does not use $this; it only reads data from $form_state and sets the template field on the cloned node, so making it static does not change the callback behavior.

Remaining tasks

Review the merge request.

User interface changes

None.

API changes

None.

Data model changes

None.

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

lolgm created an issue. See original summary.

lolgm’s picture

Status: Active » Needs review

MR !13 implements the proposed solution in this issue.

csakiistvan’s picture

Assigned: Unassigned » csakiistvan
csakiistvan’s picture

Environment

  • Drupal: 11.3.13
  • PHP: 8.5.5
  • Database: MariaDB 10.11.16
  • DDEV: v1.25.2
  • Content templates: 1.3.0
  • Quick Node Clone: enabled
  • Browser: Chrome

Prerequisites

  • Install and enable the module (pulls in media and quick_node_clone):
    ddev composer require drupal/content_templates:1.3.0
    ddev drush en content_templates -y
  • An article node with a populated image field, and a content template built from it, so the clone form shows the image with a Remove button:
    ddev drush php-eval '
    $data = file_get_contents("core/misc/druplicon.png");
    $uri = \Drupal::service("file_system")->saveData($data, "public://druplicon_test.png", 1);
    $file = \Drupal::entityTypeManager()->getStorage("file")->create(["uri" => $uri, "status" => 1]);
    $file->save();
    $n = \Drupal::entityTypeManager()->getStorage("node")->load(4);
    $n->set("field_image", ["target_id" => $file->id(), "alt" => "Test"]);
    $n->save();
    $t = \Drupal::entityTypeManager()->getStorage("content_template")->create([
      "name" => "My template", "status" => 1, "field_source" => 4,
    ]);
    $t->save();'

    (Replace 4 with the node id used.)

Root cause

Since the 1.3.0 OOP hook conversion, the Quick Node Clone form submit handler was registered as an object callback [$this, 'quickNodeCloneSubmit']. On /template/{node}/quick_clone, AJAX interactions that rebuild and cache the form (e.g. removing an image) serialize the form array; the object callback drags in the hooks service and its dependency graph, which indirectly references the database connection, so serialization fails.

Steps

  1. Apply the fix from MR !13: register the clone submit handler as a serializable static callback ([self::class, 'quickNodeCloneSubmit']) and make quickNodeCloneSubmit() a static method.
  2. Rebuild caches: ddev drush cr
  3. Log in as an administrator and open /template/4/quick_clone.
  4. In the Image field, click Remove and observe the AJAX response.

Expected results

  • The image widget rebuilds without error; the image is removed and the field returns to its empty upload state.
  • No HTTP 500 and no serialization exception in the browser console or logs.

Actual results

Before the fix, clicking Remove on the image field of the clone form returned an AJAX HTTP 500 with LogicException: The database connection is not serializable (thrown while writing the rebuilt form to the form cache), because the [$this, 'quickNodeCloneSubmit'] object callback pulled the hooks service and its database-connection reference into serialization. After the fix, the callback is a serializable static reference, the form caches cleanly, and the Remove interaction rebuilds the widget without any error.


Testing produced with the assistance of an LLM.

csakiistvan’s picture

Assigned: csakiistvan » Unassigned
Status: Needs review » Reviewed & tested by the community

a.dmitriiev made their first commit to this issue’s fork.

a.dmitriiev’s picture

Status: Reviewed & tested by the community » Needs review

I have used dependency serialization trait instead. As the approach was moved to use hook classes, using static methods means using static calls to `\Drupal` global object to call services, etc. This prevents the hook class from using the DI that is actually the main reason for using hook class.

Please review the updated MR.

jesus_md’s picture

Confirmed and tested. Reproduced the original bug on 1.3.0: cloning a node with an image field via /clone/{node}/quick_clone and removing the image via AJAX throws LogicException: The database connection is not serializable, from FormBuilder::setCache() when the form (including the ContentTemplatesHooks service reference from the submit callback) gets serialized into the form cache.

Applied MR !13 as currently pushed (DependencySerializationTrait on ContentTemplatesHooks) — the AJAX rebuild now completes without error, and the clone submission itself still works correctly afterward.

jesus_md’s picture

Status: Needs review » 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.

a.dmitriiev’s picture

Added to merge train. Thank you everyone!

Status: Fixed » Closed (fixed)

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