Problem/Motivation

In #2825557: [META] Introduce edit perspectives we are adding the concept of perspectives, which will allow to the user to select some settings for each paragraph based on its type (like width/text align or so).

Proposed resolution

Introduce a plugin system that can be selected for each paragraph type.

Remaining tasks

User interface changes

API changes

Data model changes

CommentFileSizeAuthor
#74 interdiff-2828506-72-74.txt517 bytesjohnchque
#74 introduce_a_plugin-2828506-74.patch40.86 KBjohnchque
#72 interdiff-2828506-69-72.txt2.17 KBjohnchque
#72 introduce_a_plugin-2828506-72.patch40.86 KBjohnchque
#69 interdiff-2828506-68-69.txt4.4 KBjohnchque
#69 introduce_a_plugin-2828506-69.patch39.84 KBjohnchque
#68 Screenshot from 2016-12-08 09-20-45.png57.06 KBjohnchque
#68 interdiff-2828506-66-68.txt15.44 KBjohnchque
#68 introduce_a_plugin-2828506-68.patch39.47 KBjohnchque
#67 much_blue.jpg45.42 KBBerdir
#66 Screenshot from 2016-12-07 11-10-52.png67.67 KBjohnchque
#66 interdiff-2828506-64-66.txt9.58 KBjohnchque
#66 introduce_a_plugin-2828506-66.patch38.78 KBjohnchque
#64 interdiff-2828506-61-64.txt567 bytesjohnchque
#64 introduce_a_plugin-2828506-64.patch32.23 KBjohnchque
#61 interdiff-2828506-58-61.txt47.23 KBjohnchque
#61 introduce_a_plugin-2828506-61.patch32.21 KBjohnchque
#58 interdiff-2828506-55-58.txt9.69 KBjohnchque
#58 introduce_a_plugin-2828506-58.patch34.68 KBjohnchque
#55 interdiff-2828506-52-55.txt1.62 KBjohnchque
#55 introduce_a_plugin-2828506-55.patch34.29 KBjohnchque
#52 interdiff-2828506-46-52.txt2.02 KBjohnchque
#52 introduce_a_plugin-2828506-52.patch33.83 KBjohnchque
#46 introduce_a_plugin-2828506-46.patch33.88 KBjohnchque
#45 interdiff-2828506-42-45.txt6.12 KBjohnchque
#45 introduce_a_plugin-2828506-45.patch35.43 KBjohnchque
#42 interdiff-2828506-40-42.txt11.43 KBjohnchque
#42 introduce_a_plugin-2828506-42.patch30.53 KBjohnchque
#40 introduce_a_plugin-2828506-40-interdiff.txt570 bytesBerdir
#40 introduce_a_plugin-2828506-40.patch28.89 KBBerdir
#36 interdiff-2828506-32-36.txt8.03 KBjohnchque
#36 introduce_a_plugin-2828506-36.patch28.83 KBjohnchque
#32 interdiff-2828506-27-32.txt14.21 KBjohnchque
#32 introduce_a_plugin-2828506-32.patch27.42 KBjohnchque
#27 interdiff-2828506-24-27.txt3.83 KBjohnchque
#27 introduce_a_plugin-2828506-27.patch23.05 KBjohnchque
#24 introduce_a_plugin-2828506-24.patch22.98 KBjohnchque
#21 interdiff-2828506-17-21.txt9.33 KBjohnchque
#21 introduce_a_plugin-2828506-21.patch22.96 KBjohnchque
#17 interdiff-2828506-14-17.txt2.8 KBjohnchque
#17 introduce_a_plugin-2828506-17.patch22.37 KBjohnchque
#14 interdiff-2828506-11-14.txt8.54 KBjohnchque
#14 introduce_a_plugin-2828506-14.patch21.83 KBjohnchque
#11 interdiff-2828506-8-11.txt4.27 KBjohnchque
#11 introduce_a_plugin-2828506-11.patch18.94 KBjohnchque
#8 Screenshot from 2016-11-21 09-33-27.png50.2 KBjohnchque
#8 interdiff-2828506-6-8.txt4.36 KBjohnchque
#8 introduce_a_plugin-2828506-8.patch16.62 KBjohnchque
#6 interdiff-2828506-4-6.txt20.6 KBjohnchque
#6 introduce_a_plugin-2828506-6.patch14.88 KBjohnchque
#4 interdiff-2828506-3-4.txt2.81 KBjohnchque
#4 introduce_a_plugin-2828506-4.patch8.57 KBjohnchque
#3 introduce_a_plugin-2828506-3.patch7.7 KBjohnchque
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

yongt9412 created an issue. See original summary.

johnchque’s picture

Assigned: Unassigned » johnchque
johnchque’s picture

First step for adding the plugin system.

johnchque’s picture

Added all the elements needed for making the plugin system work. We now need to decide at least one plugin to implement. Not really sure if we also should add the perspectives tab here.

miro_dietiker’s picture

Status: Needs review » Needs work

One important thing about naming:
We introduced the idea when we learned that we need to separate perspectives "Content" and "Layout".
Further research shows that it's more Content and Non-Content and all kinds of properties. Layout is a good example.

We will use the field system to assign fields or extra fields to a specific perspective. Managing those fields is done on "manage form display" table with parent grouping like core does (for instance block regions).
A plugin will extend the field definitions and offer its properties / form as extra fields.
They will be pre-assigned to a perspective, but the user is free to place them anywhere.

So i propose this terminology and definition:
We introduce ParagraphTypeFeature plugins.
Each plugin is likely a collection of settings and can be enabled on paragraph types.
Enabled plugins can then offer settings.
Based on these settings, the plugin determines what extra fields it offers to form management and display.
The plugin is deeply integrated into:
Paragraph type settings, Field management (form display, formatter output), Paragraph widget (field display, validation, submit)
Thus implementing lots of methods to alter all the elements.

In order to save properties, we use a single serialized field for all plugins. This field needs to be added to the paragraphs entity as base field.

A paragraph type can enable multiple features. All enabled plugins are triggered to extend the paragraphs. Plugins thus need to have (static) weight to deal with dependency problems (a plugin adding grid extra properties will be called after the basic grid plugin).

Let's simply implement a test plugin first. Enable it on a test paragraph type. Make it provide a field. Make it appear in the form. Make sure submission is persisted. And test cover it.

The perspective tab (UI element) is a separate issue. Possibly multiple.
We could also put configurability of those properties (manageme form display) into a follow-up.
Introducing advanced plugin settings that appear in paragraph type configuration can also be a follow-up.

johnchque’s picture

OK, made some progress, adding a table in the ParagraphsTypeForm so the user can enable the plugins for that Paragraph type. After that I gonna add the ConfigurationForm or just the form function to add the fields that the plugin provides.
Still needs tests.

miro_dietiker’s picture

Status: Needs review » Needs work

Adding settings per plugin can be added in a follow-up.
Let's first complete a plugin in its most simple form. This means the example plugin needs to provide a form item on the paragraph, its data needs to be stored and test covered.

+++ b/src/Entity/Paragraph.php
@@ -296,6 +296,10 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
+    $fields['features'] = BaseFieldDefinition::create('string')

I'd say, the plugin provides a feature (although feature clashes with the feature module) - however what we store are the serialised settings / properties, not the feature itself.

johnchque’s picture

Just wanted to show my progress, now we can show the fields provided by the plugins. :)

Working currently with serialization. :)

Status: Needs review » Needs work

The last submitted patch, 8: introduce_a_plugin-2828506-8.patch, failed testing.

The last submitted patch, 8: introduce_a_plugin-2828506-8.patch, failed testing.

johnchque’s picture

Made some other changes, now we store the values of the plugins serialized :) Just need to write tests now. :)

Status: Needs review » Needs work

The last submitted patch, 11: introduce_a_plugin-2828506-11.patch, failed testing.

The last submitted patch, 11: introduce_a_plugin-2828506-11.patch, failed testing.

johnchque’s picture

Almost working, I don't know why but when saving the first time a new created paragraphs, the fields of the plugin are not stored, because for some reason the "unserializedData" variable is not set. (I think we are still saving more than once). When saving again the values are stored.

Status: Needs review » Needs work

The last submitted patch, 14: introduce_a_plugin-2828506-14.patch, failed testing.

The last submitted patch, 14: introduce_a_plugin-2828506-14.patch, failed testing.

johnchque’s picture

Some other changes, leftovers.

Status: Needs review » Needs work

The last submitted patch, 17: introduce_a_plugin-2828506-17.patch, failed testing.

The last submitted patch, 17: introduce_a_plugin-2828506-17.patch, failed testing.

miro_dietiker’s picture

  1. +++ b/src/Annotation/ParagraphsTypeFeatureBuilder.php
    @@ -0,0 +1,40 @@
    +class ParagraphsTypeFeatureBuilder extends Plugin {
    

    Compared to other plugins, by example:

    class EntityType extends Plugin {
    

    If we name the concept "Feature" then it's the Feature that is a plugin, not the Builder.

  2. +++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
    @@ -1115,6 +1129,10 @@ class InlineParagraphsWidget extends WidgetBase {
    +          $paragraphs_entity->setUnserializedSettings($item['feature']);
    

    Serializing and unserializing is a matter of putting the lifetime object into storage and loading it. That needs to be abstracted away.

    Users of this settings should not care how it is stored. They just want to call getSettings(), setSettings() - Or should we use get/setFeatureSettings()?

    Be careful about the whole lifecycle including cache serialization and fetching (__sleep, __wakeup). But i would expect it is not a problem.
    Dunno if we should override \unserialize()?

johnchque’s picture

Changes made based on comments above, still doesn't save at first.

Status: Needs review » Needs work

The last submitted patch, 21: introduce_a_plugin-2828506-21.patch, failed testing.

The last submitted patch, 21: introduce_a_plugin-2828506-21.patch, failed testing.

johnchque’s picture

Status: Needs review » Needs work

The last submitted patch, 24: introduce_a_plugin-2828506-24.patch, failed testing.

The last submitted patch, 24: introduce_a_plugin-2828506-24.patch, failed testing.

johnchque’s picture

Now we just display the plugins table when there are at least one available to select.

Status: Needs review » Needs work

The last submitted patch, 27: introduce_a_plugin-2828506-27.patch, failed testing.

The last submitted patch, 27: introduce_a_plugin-2828506-27.patch, failed testing.

Berdir’s picture

  1. +++ b/src/Entity/Paragraph.php
    @@ -119,6 +125,46 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +  public function getFeatureSettings($key = []) {
    +    if ($this->get('feature_settings')->value) {
    +      $this->unserializedFeatureSettings = unserialize($this->get('feature_settings')->value);
    +    }
    +    else {
    +      $this->unserializedFeatureSettings = [];
    +    }
    +    if (empty($key)) {
    +      return $this->unserializedFeatureSettings;
    +    }
    +    return NestedArray::getValue($this->unserializedFeatureSettings, $key);
    

    Mixing gettig all and a specific key in a single method is bad practise. Use a getFeatureSettings() and getFeatureSetting() instead.

    Also, the unserialized logic seems backwards. Every time you call it, you replace unserializedFeatureSettings again. You must only do that once per object lifetime, so check it for !== null first.

    example that should be tested:
    1. new paragraphs, set something
    2. save
    3. load again, get feature setting
    4. set new value
    5. get must return the new value.

    Additionally, as I understand, those settings are always for a specific plugin. That means we will *always* have at least the plugin_id. And we shouldn't mix that into the key but have a $plugin_id and separate $keys argument.

  2. +++ b/src/Entity/Paragraph.php
    @@ -119,6 +125,46 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +  public function setFeatureSettings($feature_settings) {
    +    // Set feature settings field.
    +    $this->unserializedFeatureSettings = $feature_settings;
    

    same here, enforce that the top level is always grouped by plugin_id. We might not even need an API that returns/sets for all plugins, or if, then as a separate method.

  3. +++ b/src/Form/ParagraphsTypeForm.php
    @@ -41,7 +70,43 @@ class ParagraphsTypeForm extends EntityForm {
    +        '#empty' => t('There are no plugins yet. Add a plugin.'),
    

    this empty message doesn't make sense, we don't add the table if there are no plugins?

  4. +++ b/src/ParagraphsTypeFeatureInterface.php
    @@ -0,0 +1,24 @@
    +   */
    +  public function buildFeaturesForm(Paragraph $paragraphs_entity);
    

    we should probably model this after the build/validate/submitConfigurationForm() pattern except we also pass in the paragraph entity. (not sure we need $paragraphs_entity as a variable name)

  5. +++ b/src/ParagraphsTypeFeatureManager.php
    @@ -0,0 +1,60 @@
    +    // @todo define settings for the plugins.
    +    $this->config = $config_factory->get('paragraphs.paragraphs_type');
    

    that's now how this works? their settings are stored in config entities, so if you want to inject something it is the entity storage for paragraph type entities but I would skip that until we actually need it.

  6. +++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
    @@ -592,6 +592,20 @@ class InlineParagraphsWidget extends WidgetBase {
    +      $paragraphs_type = ParagraphsType::load($paragraphs_entity->getType());
    +      if ($paragraphs_type) {
    +        $config = \Drupal::config('paragraphs.paragraphs_type.' . $paragraphs_type->id());
    

    same here, you want something like $paragraphs_type->getFeaturePlugins(), not read the raw config.

    We should also use the LazyPluginCollection system I think, see FilterFormat for a good/similar example.

  7. +++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
    @@ -1059,6 +1073,9 @@ class InlineParagraphsWidget extends WidgetBase {
    +        if (isset($item['feature'])) {
    +          $display->extractFormValues($entity, $element['feature'], $form_state);
    +        }
    

    display is about fields, so I don't think this will work. If we do add the feature of adding fields in there, it will need to be done through #groups and fields will need to live top-level. you need to call the validate/submit methods on the plugin instead.

  8. +++ b/src/Entity/Paragraph.php
    @@ -119,6 +125,46 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +
    +    if ($this->unserializedFeatureSettings) {
    +      $this->set('feature_settings', serialize($this->unserializedFeatureSettings));
    +    }
    +    else {
    +      $this->set('feature_settings', serialize([]));
    +    }
    +  }
    +
    

this is the reason you lose your data.

It is related to the stuff above, and you will need to think about this here as well.

In short, empty array != NULL. If it is NULL, then you must *not* change it, as you never unserialized. Except possibly setting the default value, but you should just use setDefaultValue() on the field definition so it is initialized with the serialized empty array.

You can test test this easily be saving a paragraph with some settings, load again and save without accessing them. Your settings will be lost.

The reason that it only happens on new paragraphs is because our needsSave() logic has a bug.

We save new paragraphs twice. Once because it is new and needs to be saved because of that and a second time because needsSave() is still TRUE.

An easy workaround for that is not setting needsSave() for a new entity. A better fix would be to ensure that needsSave() is set to FALSE in Paragraph::postSave().

Berdir’s picture

So this makes it pretty clear that we absolutely need kernel/API tests for handling feature settings. Try all kinds of saving, loading, updating, loading again, resave without changing and verifying all those operations result in correct data. That should help you find and fix a lot of bugs that will be much harder to debug in UI tests.

johnchque’s picture

Added the plugin collection thingy, still not sure about the setFeatureSettings, actually when I build the form it ensures the plugin di to be part of the array. I tried both workarounds suggested but the values are still not saved at first. :/

Status: Needs review » Needs work

The last submitted patch, 32: introduce_a_plugin-2828506-32.patch, failed testing.

The last submitted patch, 32: introduce_a_plugin-2828506-32.patch, failed testing.

Berdir’s picture

  1. +++ b/src/Entity/Paragraph.php
    @@ -135,34 +135,60 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +   * {@inheritdoc}
    +   */
    +  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
    +    $this->setNeedsSave(FALSE);
    +    parent::postSave($storage, $update);
    

    fine to test, but we should do a dedicated issue for this, with test coverage (not quite sure yet how, could be as simple as calling paragraphs->save() and then making sure that needsSave() returns FALSE)

  2. +++ b/src/Entity/Paragraph.php
    @@ -135,34 +135,60 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +    if ($this->unserializedFeatureSettings == NULL) {
    

    array == NULL => TRUE
    array === NULL => FALSE

    you need type safe checks.

johnchque’s picture

Made some changes, still not saving at first, gonna open an issue for needsSave(). Not really sure if it is Ok like this, should we define the enabled/disabled setting for each plugin? Not sure about how to define the defaults for plugins.

Status: Needs review » Needs work

The last submitted patch, 36: introduce_a_plugin-2828506-36.patch, failed testing.

The last submitted patch, 36: introduce_a_plugin-2828506-36.patch, failed testing.

johnchque’s picture

Status: Needs work » Needs review

Test fail due to it doesn't save at first.

Berdir’s picture

You're still incorrectly always setting the feature settings back. As I said, you must only do that when you actually unserialized your settings. And then it works just fine* ;)

* Fine as far as that the test is now green. I haven't really reviewed the last patches yet.

johnchque’s picture

Thank you, I gonna write more tests for it. :)

johnchque’s picture

Status: Needs review » Needs work

The last submitted patch, 42: introduce_a_plugin-2828506-42.patch, failed testing.

The last submitted patch, 42: introduce_a_plugin-2828506-42.patch, failed testing.

johnchque’s picture

Added kernel tests. Expanded the UI tests a bit. Added a second test plugin.

johnchque’s picture

The last submitted patch, 45: introduce_a_plugin-2828506-45.patch, failed testing.

The last submitted patch, 45: introduce_a_plugin-2828506-45.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 46: introduce_a_plugin-2828506-46.patch, failed testing.

The last submitted patch, 46: introduce_a_plugin-2828506-46.patch, failed testing.

johnchque’s picture

Status: Needs work » Needs review

Passing all tests.

johnchque’s picture

When edit mode is closed the feature fields where still displayed, now they are also hidden.

Status: Needs review » Needs work

The last submitted patch, 52: introduce_a_plugin-2828506-52.patch, failed testing.

The last submitted patch, 52: introduce_a_plugin-2828506-52.patch, failed testing.

johnchque’s picture

Sorry, test fixed according to new module changes.

miro_dietiker’s picture

We will need a META issue for everything related to the plugins and make all current related issues its children.

miro_dietiker’s picture

Status: Needs review » Needs work

A first try to review this monster... :-)

  1. +++ b/config/schema/paragraphs_type.schema.yml
    @@ -8,6 +8,19 @@ paragraphs.paragraphs_type.*:
    +          weight:
    +            type: integer
    +            label: 'Weight'
    

    We need weight per plugin (global - to guarantee a coherent sequence in execution), not per plugin per paragraph type.

  2. +++ b/src/Annotation/ParagraphsTypeFeature.php
    @@ -0,0 +1,43 @@
    +   * The human-readable name of the paragraphs type layout builder.
    
    +++ b/src/ParagraphsTypeFeatureInterface.php
    @@ -0,0 +1,51 @@
    +   * This method is responsible for building the layout settings for each
    +   * Paragraph so the user can set special layouts.
    

    Let's use "feature" consistently.

  3. +++ b/src/Entity/Paragraph.php
    @@ -128,6 +136,67 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +    // If un-serialized settings are already set then do not overwrite them.
    

    Outdated comment.

  4. +++ b/src/Entity/Paragraph.php
    @@ -128,6 +136,67 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +  public function setFeaturesSettings($settings) {
    ...
    +  public function setFeatureSettings($plugin_id, $settings) {
    

    Misleading naming. It took me multiple minutes to realise that the names differ.

  5. +++ b/src/Entity/ParagraphsType.php
    @@ -54,4 +55,39 @@ class ParagraphsType extends ConfigEntityBundleBase implements ParagraphsTypeInt
    +   * The ParagraphsType plugins.
    ...
    +  public $feature_plugins = [];
    

    I guess the ParagraphsTypeFeature plugins. But only the names or what?

  6. +++ b/src/Entity/ParagraphsType.php
    @@ -54,4 +55,39 @@ class ParagraphsType extends ConfigEntityBundleBase implements ParagraphsTypeInt
    +      $this->featureCollection = new ParagraphsTypeFeatureCollection(\Drupal::service('plugin.manager.paragraphs_type.feature'), $this->feature_plugins);
    

    I first thought: why not on constructur? Because selected types could change?
    But then realised that if feature_plugins change, it is not reinitialised.

  7. +++ b/src/Entity/ParagraphsType.php
    @@ -54,4 +55,39 @@ class ParagraphsType extends ConfigEntityBundleBase implements ParagraphsTypeInt
    +  public function getFeaturePlugin($instance_id) {
    ...
    +    return $this->featureCollection->get($instance_id);
    
    +++ b/src/ParagraphsTypeFeatureBase.php
    @@ -0,0 +1,127 @@
    +    $paragraphs_entity->setFeatureSettings($this->getPluginId(), $values);
    

    Not sure if the base should set all of those. I thought let's have each setting explicitly set by each plugin.

  8. +++ b/src/ParagraphsTypeFeatureCollection.php
    @@ -0,0 +1,77 @@
    +   * Retrieves filter definitions and creates an instance for each filter.
    

    filter?

  9. +++ b/src/ParagraphsTypeFeatureCollection.php
    @@ -0,0 +1,77 @@
    +      // Do not allow the null filter to be used directly, only as a fallback.
    +      unset($this->definitions['filter_null']);
    

    null filter?

  10. +++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
    @@ -620,14 +620,30 @@ class InlineParagraphsWidget extends WidgetBase {
    +          if ($config = $paragraphs_type->getFeaturePlugins()) {
    ...
    +              if ($plugin->getConfiguration()['enabled'] == TRUE) {
    

    Why is there no method that returns the enabled ones only?

  11. +++ b/tests/modules/paragraphs_test/src/Plugin/paragraphs_type/Feature/TestFeaturePlugin.php
    @@ -0,0 +1,32 @@
    +      '#title' => $this->t('Label'),
    ...
    +      '#description' => $this->t("Label for the Paragraphs type."),
    

    Bad example.

  12. +++ b/tests/modules/paragraphs_test/src/Plugin/paragraphs_type/Feature/TestFeaturePluginNotRequired.php
    @@ -0,0 +1,30 @@
    +    $form['test_field_not_required'] = [
    ...
    +      '#title' => $this->t('Label'),
    ...
    +      '#description' => $this->t("Label for the Paragraphs type."),
    

    Use a different better example with a unique label.

I'll need to discuss with Berdir about how we can get this in without too much commitment.
We should clearly declare all plugins and interfaces as experimental. I guess we will change some while learning more about out plugins.

johnchque’s picture

Made some changes, added some methods, still need to find better examples.

Berdir’s picture

Status: Needs review » Needs work

#2296423: Implement layout plugin type in core has a ton of discussion about the right approach to document a currently experimental new feature in an otherwise stable method.. @internal is the current chosen approach I believe but havent' closely followed.

  1. +++ b/config/schema/paragraphs_type.schema.yml
    @@ -8,6 +8,19 @@ paragraphs.paragraphs_type.*:
    +          weight:
    +            type: integer
    +            label: 'Weight'
    

    What miro means with point 1 is that weight should be a flag on the annotation and not something that we store per paragraphs type.

    I guess that makes sense, it is not that important to have a different order in different paragraphs (we could still do that later if we'd really need it, e.g. to have a different order in the UI, but who knows how that will eventually look like).

    That also simplifies the necessary UI to support this a lot. Right now, we'd need a draggable table to control/change the weight. If we move this to the plugin definition, that goes away (doesn't exist yet, but would have to)

    I see that you started to add that, but not quite done yet it seems.

  2. +++ b/paragraphs.services.yml
    @@ -0,0 +1,5 @@
    +services:
    +  plugin.manager.paragraphs_type.feature:
    +    class: Drupal\paragraphs\ParagraphsTypeFeatureManager
    +    parent: default_plugin_manager
    

    not sure about paragraphs_type.feature or paragraphs.feature? And then same for the class names.

    Usually this is module.plugin_type, so I'd go for paragraphs.feature, not 100% sure about the class names, but I'd say same there?

  3. +++ b/src/Entity/Paragraph.php
    @@ -128,6 +136,67 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +   *
    +   * @return array
    +   *   The array of feature settings.
    +   */
    +  public function getFeaturesSettings() {
    +    if ($this->unserializedFeatureSettings === NULL) {
    +      $this->unserializedFeatureSettings = unserialize($this->get('feature_settings')->value);
    +    }
    +    if (!is_array($this->unserializedFeatureSettings)) {
    

    agreed with Miro, getFeatureSettings() vs. getFeaturesSettings() isn't very clear yet. And the documentation also doesn't really help.

    Maybe getAllFeatureSettings()? And the docs should make it clear that one is from all features and the other is for a specific plugin.

  4. +++ b/src/Entity/Paragraph.php
    @@ -128,6 +136,67 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +   * @param string $plugin_id
    +   *   The plugin id where to get the setting from.
    

    The plugin ID for which to get the settings.

    Or something like that.

  5. +++ b/src/Entity/Paragraph.php
    @@ -128,6 +136,67 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +  public function getFeatureSettings($plugin_id, $key) {
    

    Setting*s* and then a key to get a specific one is also not that great.

    And the documention can also be improved to describe $key better. See FormStateInterface::getValue() for good documentation example on $key.

    And I'd say we should then go way getFeatureSetting() here.

    Documentation for all those methods should be on ParagraphsInterface, btw.

  6. +++ b/src/Entity/Paragraph.php
    @@ -128,6 +136,67 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +   * Sets the un-serialized feature settings.
    

    The fact that we serialize is an implementation detail, you should never have to mention that in the documentation. That's obviuous enough when you pass in an array. So drop that word.

  7. +++ b/src/Entity/Paragraph.php
    @@ -128,6 +136,67 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +  public function setFeaturesSettings($settings) {
    

    type int on array when it is always an array in the argument ($key in FormState::getValue() can also be a string, but then you need to handle that), settings is always an array.

  8. +++ b/src/Form/ParagraphsTypeForm.php
    @@ -41,7 +70,42 @@ class ParagraphsTypeForm extends EntityForm {
    +      $weight = count($feature_plugins) + 1;
    +      $feature_plugins_order = [];
    +      foreach ($feature_plugins as $id => $feature_plugin) {
    +        $feature_plugins_order[$id] = [
    +          'label' => $feature_plugin['label'],
    

    this should also get easier then. We can do the sorting earlier, in findDefinitions() and then they will be in the right order already here.

  9. +++ b/src/ParagraphsTypeFeatureBase.php
    @@ -0,0 +1,120 @@
    +    $this->configuration += $this->defaultConfiguration();
    

    note that this doesn't work recursively. Doing it recursively has other drawbacks, just means we don't support nested default configuration and should keep it flat.

  10. +++ b/src/ParagraphsTypeFeatureBase.php
    @@ -0,0 +1,120 @@
    +      $container->get('config.factory'),
    +      $container->get('entity_type.manager')
    

    we don't need either of those, config is is saved in paragraph/paragraph_type and we shouldn't have to load any entities.. if some plugins do, they can still add those. so you can drop this method completely and also simplify the constructor.

  11. +++ b/src/ParagraphsTypeFeatureBase.php
    @@ -0,0 +1,120 @@
    +  public function defaultConfiguration() {
    +    return [
    +      'enabled' => FALSE,
    +    ];
    +  }
    

    I would keep this out of the actual configuration

  12. +++ b/src/ParagraphsTypeFeatureBase.php
    @@ -0,0 +1,120 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function setConfiguration(array $configuration) {
    +  }
    +
    

    we can actually implement this.

  13. +++ b/src/ParagraphsTypeFeatureBase.php
    @@ -0,0 +1,120 @@
    +   */
    +  public function validateFeaturesForm(array $form, FormStateInterface $form_state) {
    +    // TODO: Implement validateFeaturesForm() method.
    

    no need for a TODO here, just keep it empty like other methods.

    Also, I'd put empty method blocks on a single line... function bla (arguments) { }

  14. +++ b/src/ParagraphsTypeFeatureCollection.php
    @@ -0,0 +1,87 @@
    +   * {@inheritdoc}
    +   */
    +  public function sort() {
    +    $this->getAll();
    +    return parent::sort();
    +  }
    

    we don't need sorting then if we don't have per-type weights I think.

  15. +++ b/src/ParagraphsTypeFeatureCollection.php
    @@ -0,0 +1,87 @@
    +   * This is exclusively used for retrieving all the plugins available for the
    +   * current paragraph type.
    +   */
    +  public function getAll() {
    

    what does "available for the current paragraph type"? Isn't it *all*, at least for now?

  16. +++ b/src/ParagraphsTypeFeatureCollection.php
    @@ -0,0 +1,87 @@
    +   */
    +  protected function initializePlugin($instance_id) {
    +    $configuration = $this->manager->getDefinition($instance_id);
    +    // Merge the actual configuration into the default configuration.
    +    if (isset($this->configurations[$instance_id])) {
    +      $configuration = NestedArray::mergeDeep($configuration, $this->configurations[$instance_id]);
    +    }
    +    $this->configurations[$instance_id] = $configuration;
    +    parent::initializePlugin($instance_id);
    +  }
    

    this is confusing. the definition is not the configuration. we don't have configuration yet (apart from the enabled flag), we'll add that later.

  17. +++ b/src/ParagraphsTypeFeatureInterface.php
    @@ -0,0 +1,51 @@
    +interface ParagraphsTypeFeatureInterface extends PluginFormInterface, ConfigurablePluginInterface {
    

    missing docblock on the interface.

    We now hove 3 methods to control the features form, validating and saving it. But what does such a plugin actually *do* now? and how?

    We can say we do that later, then we need at least an issue and possibly a todo.

    If we can define an actually useful example (like setting the text color, I think I suggested that before) then we can discuss on how to implement that and what kind of methods we need. Specifically, to implement that, we need to be able to a) add some css file/library to the output and we need to add a class to the output based on the user selection. So this needs something in view/preprocess. Maybe start with a view() method that has the same arguments as hook_entity_view() and a preprocess() method that receives $variables?

  18. +++ b/src/ParagraphsTypeFeatureInterface.php
    @@ -0,0 +1,51 @@
    +   * @return mixed
    +   *   The modified build array that the plugin builds.
    

    modified? we don't get something passed in, so we can't relly modify anyting?

    also should always be an array, not mixed?

  19. +++ b/src/ParagraphsTypeFeatureManager.php
    @@ -0,0 +1,35 @@
    +    parent::__construct('Plugin/paragraphs_type/Feature', $namespaces, $module_handler, 'Drupal\paragraphs\ParagraphsTypeFeatureInterface', 'Drupal\paragraphs\Annotation\ParagraphsTypeFeature');
    

    as with the service name, the first folder is usually the provider/module providing the plugin, so just paragraphs. Feature or feature is then always the big question for the second folder. No strong opinion.

    Same for the cache key, alter hooks and so on.

  20. +++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
    @@ -620,14 +620,28 @@ class InlineParagraphsWidget extends WidgetBase {
    +          if ($config = $paragraphs_type->getEnabledFeaturePlugins()) {
    +            foreach ($config as $plugin_id => $plugin) {
    

    $config is a weird name for this. Why not $feature_plugins or so?

  21. +++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
    @@ -620,14 +620,28 @@ class InlineParagraphsWidget extends WidgetBase {
    +              $element['feature'][$plugin_id] = [];
    +              $display->buildForm($paragraphs_entity, $element['feature'][$plugin_id], $form_state);
    +              $element['feature'][$plugin_id] = $plugin->buildFeaturesForm($paragraphs_entity);
    

    I don't understand what the $display line is doing? we're not building an entity form here.

  22. +++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
    @@ -1190,6 +1203,13 @@ class InlineParagraphsWidget extends WidgetBase {
    +        if (isset($item['feature'])) {
    +          $paragraphs_type = ParagraphsType::load($paragraphs_entity->getType());
    +          foreach ($paragraphs_type->getEnabledFeaturePlugins() as $plugin_id => $plugin_values) {
    +            $plugin_values->submitFeaturesForm($paragraphs_entity, $item['feature'][$plugin_id]);
    

    validate doesn't seem to be called yet?

    To be able to test that, I would suggest that the test-text-color plugin would have a text field that only allows "blue" and "red" as input, anything else must result in a validation error.

    And we could then default to "blue" to have the default configuration stuff also tested.

  23. +++ b/tests/modules/paragraphs_test/src/Plugin/paragraphs_type/Feature/TestFeaturePluginNotRequired.php
    @@ -0,0 +1,37 @@
    +    $form['test_field_not_required'] = [
    +      '#type' => 'textfield',
    +      '#title' => $this->t('Label'),
    +      '#maxlength' => 255,
    +      '#default_value' => $paragraphs_entity->getFeatureSettings($this->getPluginId(), ['test_field_not_required']),
    +      '#description' => $this->t("Label for the Paragraphs type."),
    +    ];
    +    return $form;
    

    testing #required vs. not isn't a really useful test. That's default form-level validation, not specific to us.

miro_dietiker’s picture

Discussed about naming and synonyms for "feature" to avoid the ambiguous overlap with features module.

We decided to name the plugins "behavior". (and this time it's final) ;-)

johnchque’s picture

Renamed to behavior, addressed changes of previous comments.

Status: Needs review » Needs work

The last submitted patch, 61: introduce_a_plugin-2828506-61.patch, failed testing.

The last submitted patch, 61: introduce_a_plugin-2828506-61.patch, failed testing.

johnchque’s picture

Fixed tests.

Berdir’s picture

Status: Needs review » Needs work

Looks like not everything is addressed yet of my review, at least the big/major point #17 for actually doing something with those behavior settings now. And also validation isn't really covered yet I think.

Fine now, but in a case like this, where you do one huge change and also address a review, you might want to do two separate commits, so you can provide two interdiffs, or actually upload separate patches/interdiffs, one for the big rename and another to address the review and do those things separately. Listing which review points were addressed and what's still pending would also help.

  1. +++ b/src/Entity/Paragraph.php
    @@ -137,66 +135,69 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
    +  public function setBehaviorSetting($plugin_id, array $settings) {
    

    I think here it actually is Settings() as we set all settings for a behavior. The alternative would be to have $key, $value as argument, then setting might work again.

  2. +++ b/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestTextColorPlugin.php
    @@ -0,0 +1,49 @@
    +      '#default_value' => $paragraphs_entity->getBehaviorSetting($this->getPluginId(), 'text_color'),
    

    Looks like we're missing a way to set default settings? We can't use defaultConfiguration(), that will be on the paragraph type level. Maybe we could do it like third party settings and add a default value argument to this method, just like $form_state->getValue()? Downside is that we have to repeat it multiple cases but a defaultConfiguration() like system sounds complicated.

  3. +++ b/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestTextColorPlugin.php
    @@ -0,0 +1,49 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function validateBehaviorForm(array $form, FormStateInterface $form_state) {
    +    parent::validateBehaviorForm($form, $form_state);
    +  }
    +
    

    looks like the validation part isn't here yet?

johnchque’s picture

Ok, took me longer than expected, still working on the default config, will change the method name of setBehaviorSetting in the next patch and implement the validation too. By now I could modify the output with the test plugins, changing the font weight with one of them and adding a text color with the other.

oh btw, will improve the code in the BuildMultiple method to use different view modes, by now just using default.

Berdir’s picture

FileSize
45.42 KB

  1. +++ b/paragraphs.install
    @@ -180,4 +180,20 @@ function paragraphs_update_8008() {
    +
    +  $storage_definition = BaseFieldDefinition::create('string_long')
    +    ->setLabel(t('Behavior plugins'));
    +  \Drupal::entityDefinitionUpdateManager()
    +    ->installFieldStorageDefinition('behavior_plugins', 'paragraphs_type', 'paragraph', $storage_definition);
     }
    

    config entities don't have fields, this part isn't needed.

  2. +++ b/paragraphs.module
    @@ -238,4 +238,11 @@ function template_preprocess_paragraph(&$variables) {
    +
    +  $paragraph_type_id = $variables['elements']['#paragraph']->getType();
    +  $paragraph_type = ParagraphsType::load($paragraph_type_id);
    +  foreach ($paragraph_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_value) {
    

    instead of all those ::load() calls, we can just use $paragraph->type->entity, possibly also add a getPararagraphType() method or so (getType() should hae been called getTypeId(), just like node, too late now)

  3. +++ b/src/ParagraphsBehaviorInterface.php
    @@ -49,4 +50,32 @@ interface ParagraphsBehaviorInterface extends PluginFormInterface, ConfigurableP
    +   * @param &$build
    +   *   A renderable array representing the entity content. The module may add
    +   *   elements to $build prior to rendering. The structure of $build is a
    +   *   renderable array as expected by drupal_render().
    ...
    +   * @param $view_mode
    +   *   The view mode the entity is rendered in.
    +   *
    

    missing type

  4. +++ b/src/ParagraphsBehaviorInterface.php
    @@ -49,4 +50,32 @@ interface ParagraphsBehaviorInterface extends PluginFormInterface, ConfigurableP
    +   * @param \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity
    +   *   The entity object.
    

    we can say that this is actually the paragraph. I would also just name this $paragraph, it's $user and $node, not $node_entity.

  5. +++ b/src/ParagraphsBehaviorInterface.php
    @@ -49,4 +50,32 @@ interface ParagraphsBehaviorInterface extends PluginFormInterface, ConfigurableP
    +   * @return array
    +   *   A render array provided by the plugin.
    

    I don't think we need a return type as we have it per reference.

  6. +++ b/src/ParagraphsBehaviorInterface.php
    @@ -49,4 +50,32 @@ interface ParagraphsBehaviorInterface extends PluginFormInterface, ConfigurableP
    +  public function preprocess(&$variables);
    

    missing docs.

  7. +++ b/tests/modules/paragraphs_test/paragraphs_test.libraries.yml
    @@ -0,0 +1,11 @@
    +  version: VERSION
    

    VERSION is actually only support for core, but it is mis-used everywhere..

  8. +++ b/tests/modules/paragraphs_test/src/Plugin/paragraphs/Behavior/TestBoldTextPlugin.php
    @@ -45,4 +45,23 @@ class TestBoldTextPlugin extends ParagraphsBehaviorBase {
    +   * {@inheritdoc}
    +   */
    +  public function preprocess(&$variables) {
    +    if ($variables) {
    +
    +    }
    

    provide an empty default on the base class so you don't need to repeat empty methods.

johnchque’s picture

Validation added! Addressed the changes suggested above. What is missing is that buildMultiple is still using just the default view mode, will fix in the next patch and also I'm not really sure about default config, how should it be implemented? Should each plugin define their own default values? And in "#default_value" of the form of the plugin should we have some API to get the stored value instead of getting it from the entity?

Here a look of validation implementation.

johnchque’s picture

OK, added default config logic as discussed with @Berdir and also changed the view mode to use the current in the paragraph.

Berdir’s picture

+++ b/src/ParagraphViewBuilder.php
@@ -16,10 +16,10 @@ class ParagraphViewBuilder extends EntityViewBuilder {
     $build_list = parent::buildMultiple($build_list);
     foreach ($build_list as $key => $value) {
-      $display = EntityViewDisplay::load('paragraph.' . $value['#paragraph']->bundle() . '.default');
+      $display = EntityViewDisplay::load('paragraph.' . $value['#paragraph']->bundle() . '.' . $value['#view_mode']);
       $paragraph_type = $value['#paragraph']->getParagraphType();
       foreach ($paragraph_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_value) {

It is unfortunately not quite that easy. view modes/view displays have a fallback system. If you pass in view mode "full", and there is no entity view display for that, it falls back to .default. So you need to add "?: EntityViewDisplay::load(... '.default')

Looks

+++ b/paragraphs.install
@@ -181,19 +181,3 @@ function paragraphs_update_8008() {
- * Add behavior plugins fields.
- */
-function paragraphs_update_8010() {
-  $storage_definition = BaseFieldDefinition::create('string_long')
-    ->setLabel(t('Behavior settings'))
-    ->setDescription(t('The behavior plugin settings'));
-  \Drupal::entityDefinitionUpdateManager()
-    ->installFieldStorageDefinition('behavior_settings', 'paragraph', 'paragraph', $storage_definition);

now you deleted too much., *config entities* don't have field definitions. content entities do. this part you need.

We also need to make sure we initialize it with a correct value. That is a bit tricky, we need to set the initial in the schema, see FileEntityStorageSchema for type.

Install with the old version including demo so you have some content, then update, run updates and make sure you don't get any notices after that. and that status page doesn't report any pending changes for paragraphs.

miro_dietiker’s picture

Status: Needs review » Needs work
johnchque’s picture

Added update function again, addressed changes of comments above. :) I tested updating from dev to this branch and the plugins are working, didn't found any update notice after run db updates. :)

Berdir’s picture

Status: Needs review » Needs work
+++ b/src/ParagraphStorageSchema.php
@@ -34,6 +34,10 @@ class ParagraphStorageSchema extends SqlContentEntityStorageSchema {
+
+    if ($storage_definition->getName() == 'behavior_settings') {
+      $schema['fields']['behavior_settings']['initial'] = 'undefined';
+    }

that is not what I said :)

'undefined' is not a valid serialized string. When I run the update on a site with demo installed and then enable paragraphs_test and enable the two plugins for example for text and then edit a paragraph, I get nasty warnings:

Notice: unserialize(): Error at offset 0 of 9 bytes in Drupal\paragraphs\Entity\Paragraph->getAllBehaviorSettings() (line 152 of modules/paragraphs/src/Entity/Paragraph.php).

Look at how the string looks like for an empty array for a new paragraph that you create, that's the initial you want, something like "a:{}" or so.

johnchque’s picture

Oops, sorry, changed as serializing an empty array []. :)

johnchque’s picture

I can confirm that with the last patch there are no errors displayed, I was able to reproduce with the previous patch. :)

Berdir’s picture

Status: Needs review » Reviewed & tested by the community

Ok, then, lets do this ;)

miro_dietiker’s picture

Status: Reviewed & tested by the community » Fixed

Commiting. Let's move forward and work in follow-ups to improve the monster. ;-)

I improved documentation a bit and added some explanation prepending each added code logic block.

Still a few questions and if we should improve those things:
- I see lots of public methods. Should we make some protected?
- I see no @internal declaration. Are we ready to guarantee them with long term stability?

The type management uses a checkbox element with a label. I thought we are typically using two separate elements?
I see a hardcoded "a:0:{}". I thought we should do a serialize([]).

I'm not 100% sure about naming, when to use ParagraphsXYZ and when ParagraphXYZ. We use now ParagraphViewBuilder, but ParagraphsBehaviorInterface.

Please discuss and create follow-ups.

Primsi’s picture

In https://www.drupal.org/node/2828506#comment-11788732 Miro mentioned a possible follow up regarding plugin configuration forms. While implementing behaviour pluginsit became clear that some settings cannot live in the per-paragraph configuration form (ie background image field selection in #2835789: Implement background image plugin, starting code).

IMHO providing per applied behaviour configuration forms would make sense. And if an even more global settings level is required, it can be provided by the implementing module independently.

Primsi’s picture

Status: Fixed » Closed (fixed)

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

fgm’s picture

FWIW, this commit broke one of our sites, with this stack (no custom code in it, as you can see):

The website encountered an unexpected error. Please try again later.
Error: Call to a member function bundle() on string in Drupal\paragraphs\ParagraphViewBuilder->buildMultiple() (line 21 of modules/contrib/paragraphs/src/ParagraphViewBuilder.php).
Drupal\paragraphs\ParagraphViewBuilder->buildMultiple(Array)
call_user_func(Array, Array) (Line: 376)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 448)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 195)
Drupal\Core\Render\Renderer->render(Array) (Line: 490)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 52)
__TwigTemplate_d555d99ccef10bf35a317da66d94259be8c7b20c26e80aa2080e587839bb62aa->doDisplay(Array, Array) (Line: 432)
Twig_Template->displayWithErrorHandling(Array, Array) (Line: 403)
Twig_Template->display(Array) (Line: 411)
Twig_Template->render(Array) (Line: 64)
twig_render_template('modules/contrib/paragraphs/templates/paragraph.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('paragraph', Array) (Line: 435)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 195)
Drupal\Core\Render\Renderer->render(Array) (Line: 490)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 71)
__TwigTemplate_5b03bca200df2d5ba1ca47d2e222b86347639abbdcd0d8d587ebad065efba4c8->doDisplay(Array, Array) (Line: 432)
Twig_Template->displayWithErrorHandling(Array, Array) (Line: 403)
Twig_Template->display(Array) (Line: 411)
Twig_Template->render(Array) (Line: 64)
twig_render_template('core/themes/classy/templates/field/field.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 435)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 448)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 195)
Drupal\Core\Render\Renderer->render(Array) (Line: 490)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 114)
__TwigTemplate_4ae59022820823361d314fdb0db03eda142ac3295eb1399a7323e963e3d3bf5a->doDisplay(Array, Array) (Line: 432)
Twig_Template->displayWithErrorHandling(Array, Array) (Line: 403)
Twig_Template->display(Array) (Line: 411)
Twig_Template->render(Array) (Line: 64)
twig_render_template('core/themes/bartik/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 435)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 195)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 226)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 574)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 227)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 117)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) (Line: 111)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('kernel.view', Object) (Line: 149)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 64)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 99)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 78)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 50)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 656)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Berdir’s picture

I think there's an issue about that somewhere in the queue.

stBorchert’s picture

I ran into this error, too, but couldn't find a proper issue. So I created a new one and added a patch: #2867069: Error: Call to a member function bundle() on null in ParagraphViewBuilder.