Problem/Motivation

Quoting #3228465: Final pass over the public API prior to creating core merge request, point 6:

We should probably remove the settingsForm() method from \Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface and instead make CKEditor5PluginConfigurableInterface also extend \Drupal\Core\Plugin\PluginFormInterface. It's not because CKEditor 4 had that in \Drupal\ckeditor\CKEditorPluginConfigurableInterface that we need to repeat that pattern for CKEditor 5. (That interface was committed in February 2013, and PluginFormInterface was committed in August 2013, which explains this.)

Steps to reproduce

N/A

Proposed resolution

TBD

Remaining tasks

TBD

User interface changes

TBD

API changes

TBD

Data model changes

None.

CommentFileSizeAuthor
#12 Screenshot 2021-08-18 at 15.48.17.png971.5 KBWim Leers

Issue fork ckeditor5-3228477

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:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

Wim Leers’s picture

Title: CKEditor5PluginConfigurableInterface should extend PluginFormInterface instead of adding ::settingsForm() » CKEditor5PluginConfigurableInterface should extend PluginFormInterface and ConfigurableInterface instead of adding ::settingsForm()
Assigned: Unassigned » Wim Leers

I dug into this, first concluded it does not make sense, and then eventually after spending a few hours on a PoC, I concluded it totally makes sense, and is more consistent with the rest of Drupal core/follows more best practices.

Tried, this is impossible. We need these subforms to be aware of the Text Editor config entity this form is generated for. We can't add a new argument to this method.
Well … what if $form_state->getFormObject()->getEntity() actually returned the text editor config entity it's supposed to return?!

Furthermore, the submitConfigurationForm() part definitely does not make sense — since this will not be submitted by itself.
That's actually fine … plugin system maintainer @tim.plunkett has already set an example in 2014, in #1846172: Replace the actions API, where \Drupal\Core\Action\Plugin\Action\EmailAction::submitConfigurationForm() does

  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    $this->configuration['recipient'] = $form_state->getValue('recipient');
    $this->configuration['subject'] = $form_state->getValue('subject');
    $this->configuration['message'] = $form_state->getValue('message');
  }

Clearly, that's not saving anything! Same in the more recent \Drupal\Core\Layout\LayoutDefault::submitConfigurationForm().

Finally, validateConfigurationForm() actually invites writing form validation logic, but it really should have validation constraints. We could simply not call the validation method…

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Active » Needs review
Related issues: +#3222852: <dl> <dt> <dd> by introducing "Manually editable HTML tags" configuration on Source Editing

Note that this branch is relative to #3222852: <dl> <dt> <dd> by introducing "Manually editable HTML tags" configuration on Source Editing — i.e. it includes its full commit history, because that's where this problem became apparent. This should either land after #3222852, or it should be rebased to apply to latest 1.0.x — either way works.

Happy to push this forward more, but would first like reviews, especially from @tim.plunkett.

tim.plunkett’s picture

TypeError: implode(): Argument #2 ($array) must be of type ?array, string given in implode() (line 29 of /Users/tim.plunkett/www/d8/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php)

tim.plunkett’s picture

I pushed up a commit that starts down the path of ConfigurableInterface, but I did not finish everything. My worries now are about the synchronization of configuration from the plugin back to the editor.
In the standard ConfigEntity:plugins relationship, EntityWithPluginCollectionInterface is used.
See \Drupal\Core\Config\Entity\ConfigEntityBase::preSave() and
\Drupal\Core\Config\Entity\ConfigEntityBase::set()
(and __sleep() and calculateDependencies() handle it too, which is nice!)

tim.plunkett’s picture

Attempted a rebase after dl/dd/dt landed, but not confident enough to overwrite the existing branch. So posted a new one for now.

tim.plunkett’s picture

Here's the last fail:

"
An AJAX HTTP error occurred.
HTTP Result Code: 200
Debugging information follows.
Path: /admin/config/content/formats/manage/ckeditor5?destination=/admin/config/content/formats&ajax_form=1&_wrapper_format=drupal_ajax
StatusText: OK
ResponseText: TypeError: Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing::getElementsSubset(): Return value must be of type array, string returned in Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing-&gt;getElementsSubset() (line 59 of /Users/tim/www/d8/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php)."

allowed_values is sometimes an array when it should always be a string.

This doesn't happen in HEAD because of a call to HTMLRestrictionsUtilities::allowedElementsStringToPluginElementsArray() in \Drupal\ckeditor5\AdminUi::getSubmittedEditorAndFilterFormat()
This MR moves that call to \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing::validateConfigurationForm(), which is semantically the best place for it. Except that is still somehow not taking effect (or happening too late, too early, etc).

Because we constantly throw away and rebuild these plugins (not sure why, but we do), it pulls the values from the editor entity.
At some point we're getting a mismatch in data, the entity (incorrectly) has the allowed_values as a string, and the plugin is then created with invalid config.

I don't know how to best figure out where/why that is happening. Any advice/help is welcome.

Wim Leers’s picture

Because we constantly throw away and rebuild these plugins (not sure why, but we do)

I noticed that too while debugging… is it because we are not yet following certain best practices? Or is it because we did not yet optimize things at all?

tim.plunkett’s picture

When you have a config entity containing a set of configured plugins, the best practices are clear: at runtime and in forms the plugins always know their correct configuration. This means having implementations of submitConfigurationForm() doing things like $this->configuration['foo'] = $form_state->getValue('foo'); and then whoever calls submitConfigurationForm() would then call getConfiguration() and store that.

I already did the other part of instantiating the plugins with their config.

The final part would be storing the instantiated plugins so that the build*, validate*, and submit* calls are all to the same instance.


The tricky part here is that it's not a config entity calling to these plugins, it's a plugin itself (CKEditor5.php).

Wim Leers’s picture

Reproduced:

Digging deeper… found the root cause.

  public function getPlugin(string $plugin_id, ?EditorInterface $editor): CKEditor5PluginInterface {
    $configuration = [];
    if ($editor) {
      $configuration = $editor->getSettings()['plugins'][$plugin_id] ?? [];
    }
    return $this->createInstance($plugin_id, $configuration);
  }

(introduced in https://git.drupalcode.org/project/ckeditor5/-/merge_requests/93/diffs?c...)

Except that is still somehow not taking effect (or happening too late, too early, etc).

That is exactly it: it's happening "too late". Because we introduced \Drupal\ckeditor5\AdminUi::getSubmittedEditorAndFilterFormat() which operates on $form_state->getValues() and/or $form_state->getUserInput() to make the dynamic AJAX form stuff possible at all (at least back then), form validation logic has not yet run, and hence these transformations have not yet run.

Note I said "at least back then". Maybe by now we can actually refactor AdminUi::getSubmittedEditorAndFilterFormat() away? 🤞🤞🤞

But to keep scope manageable here, I'd say we could instead be invoking from within AdminUi::getSubmittedEditorAndFilterFormat()each plugin's ::validateConfigurationForm() and ::submitConfigurationForm(). Not pretty, not elegant, but a stepping stone?

Actually, why is ::submitConfigurationForm() not yet implemented? I thought we'd match EmailAction::submitConfigurationForm()'s pattern I quoted in #2?

Wim Leers’s picture

Assigned: Unassigned » tim.plunkett
Status: Needs review » Needs work

Met with Tim, discussed this in detail, he's going to push it forward further :)

Updating issue metadata to reflect that.

tim.plunkett’s picture

Status: Needs work » Needs review

I think AdminUi::getSubmittedEditorAndFilterFormat() will live to see another day, but I've reduced its usage for now.

I'm not really happy with what I came up with.
In HEAD, the $form_state is the canonical place to look for the values (except when $editor->setSettings() is called occasionally...)
The ideal would be that the plugin config would be the place until the very end, when $editor->setSettings() is called to store it all.

With this MR as it is, we're kinda in the middle of both.
But, the important thing IMO is that the API is correct, and the processing of that can be resolved later.

Pushed up what I had, I will be around for a bit to fix up small things, but I won't be able to fully pick this up again until Monday.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

@tim.plunkett: That looks WONDERFUL! Review posted. Basically only nits.

I'm happy with this going as-is, so: RTBC.

I'm signing off for today. Feel free to merge. If you don't: are you okay with me reviewing and merging this tomorrow, and pushing this to completion to the best of my ability? It's green thanks to you, it's looking waaaaaay better than in HEAD thanks to you, and so I'd love to get it in ASAP :) (Ideally I wake up to this having been merged! :D)

tim.plunkett’s picture

Status: Reviewed & tested by the community » Fixed

Merged! Thanks @Wim Leers for all of the help on this.

Wim Leers’s picture

I did not like that last commit making the new CKEditor5PluginManager::getPlugin() public, but that's it. Tracking that in #3228465-7: Final pass over the public API prior to creating core merge request.

Actually … I do not like the pre-existing public function getPlugins() on CKEditor5PluginManager either. And I think you just made it easier to solve that. There's a lot of public API on the plugin manager that shouldn't be public API. That's already captured on #3228465 fortunately.

In digging into this, I just realized something: \Drupal\ckeditor5\Plugin\Editor\CKEditor5 is a plugin instance. Since this commit, it now contains

  /**
   * The set of configured CKEditor 5 plugins.
   *
   * @var \Drupal\ckeditor5\Plugin\CKEditor5PluginInterface[]
   */
  private $plugins;

But those are constructed using

  private function getPlugins(EditorInterface $editor): array {
    if (!$this->plugins) {
      $this->plugins = $this->ckeditor5PluginManager->getPlugins($editor);
    }
    return $this->plugins;
  }

… which means $this->plugins is now tied to a EditorInterface config entity: the plugin instances in the text editor plugin instance are coupled to that particular text editor config entity. But the text editor plugin instance is not coupled to a particular text editor config entity. Effectively we've made the Text Editor plugin instance stateful where it stateless before.

Creating an issue…

Status: Fixed » Closed (fixed)

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