Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
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
Comment | File | Size | Author |
---|---|---|---|
#74 | interdiff-2828506-72-74.txt | 517 bytes | johnchque |
#74 | introduce_a_plugin-2828506-74.patch | 40.86 KB | johnchque |
#72 | interdiff-2828506-69-72.txt | 2.17 KB | johnchque |
#72 | introduce_a_plugin-2828506-72.patch | 40.86 KB | johnchque |
#69 | interdiff-2828506-68-69.txt | 4.4 KB | johnchque |
Comments
Comment #2
johnchqueComment #3
johnchqueFirst step for adding the plugin system.
Comment #4
johnchqueAdded 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.
Comment #5
miro_dietikerOne 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.
Comment #6
johnchqueOK, 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.
Comment #7
miro_dietikerAdding 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.
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.
Comment #8
johnchqueJust wanted to show my progress, now we can show the fields provided by the plugins. :)
Working currently with serialization. :)
Comment #11
johnchqueMade some other changes, now we store the values of the plugins serialized :) Just need to write tests now. :)
Comment #14
johnchqueAlmost 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.
Comment #17
johnchqueSome other changes, leftovers.
Comment #20
miro_dietikerCompared to other plugins, by example:
If we name the concept "Feature" then it's the Feature that is a plugin, not the Builder.
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()?
Comment #21
johnchqueChanges made based on comments above, still doesn't save at first.
Comment #24
johnchqueRe rolled.
Comment #27
johnchqueNow we just display the plugins table when there are at least one available to select.
Comment #30
BerdirMixing 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.
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.
this empty message doesn't make sense, we don't add the table if there are no plugins?
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)
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.
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.
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.
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().
Comment #31
BerdirSo 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.
Comment #32
johnchqueAdded 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. :/
Comment #35
Berdirfine 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)
array == NULL => TRUE
array === NULL => FALSE
you need type safe checks.
Comment #36
johnchqueMade 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.
Comment #39
johnchqueTest fail due to it doesn't save at first.
Comment #40
BerdirYou'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.
Comment #41
johnchqueThank you, I gonna write more tests for it. :)
Comment #42
johnchqueDid some clean up, I am writing kernel tests now. :)
Comment #45
johnchqueAdded kernel tests. Expanded the UI tests a bit. Added a second test plugin.
Comment #46
johnchqueOops, rebased.
Comment #51
johnchquePassing all tests.
Comment #52
johnchqueWhen edit mode is closed the feature fields where still displayed, now they are also hidden.
Comment #55
johnchqueSorry, test fixed according to new module changes.
Comment #56
miro_dietikerWe will need a META issue for everything related to the plugins and make all current related issues its children.
Comment #57
miro_dietikerA first try to review this monster... :-)
We need weight per plugin (global - to guarantee a coherent sequence in execution), not per plugin per paragraph type.
Let's use "feature" consistently.
Outdated comment.
Misleading naming. It took me multiple minutes to realise that the names differ.
I guess the ParagraphsTypeFeature plugins. But only the names or what?
I first thought: why not on constructur? Because selected types could change?
But then realised that if feature_plugins change, it is not reinitialised.
Not sure if the base should set all of those. I thought let's have each setting explicitly set by each plugin.
filter?
null filter?
Why is there no method that returns the enabled ones only?
Bad example.
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.
Comment #58
johnchqueMade some changes, added some methods, still need to find better examples.
Comment #59
Berdir#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.
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.
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?
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.
The plugin ID for which to get the settings.
Or something like that.
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.
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.
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.
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.
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.
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.
I would keep this out of the actual configuration
we can actually implement this.
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) { }
we don't need sorting then if we don't have per-type weights I think.
what does "available for the current paragraph type"? Isn't it *all*, at least for now?
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.
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?
modified? we don't get something passed in, so we can't relly modify anyting?
also should always be an array, not mixed?
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.
$config is a weird name for this. Why not $feature_plugins or so?
I don't understand what the $display line is doing? we're not building an entity form here.
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.
testing #required vs. not isn't a really useful test. That's default form-level validation, not specific to us.
Comment #60
miro_dietikerDiscussed 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) ;-)
Comment #61
johnchqueRenamed to behavior, addressed changes of previous comments.
Comment #64
johnchqueFixed tests.
Comment #65
BerdirLooks 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.
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.
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.
looks like the validation part isn't here yet?
Comment #66
johnchqueOk, 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.
Comment #67
Berdirconfig entities don't have fields, this part isn't needed.
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)
missing type
we can say that this is actually the paragraph. I would also just name this $paragraph, it's $user and $node, not $node_entity.
I don't think we need a return type as we have it per reference.
missing docs.
VERSION is actually only support for core, but it is mis-used everywhere..
provide an empty default on the base class so you don't need to repeat empty methods.
Comment #68
johnchqueValidation 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.
Comment #69
johnchqueOK, added default config logic as discussed with @Berdir and also changed the view mode to use the current in the paragraph.
Comment #70
BerdirIt 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
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.
Comment #71
miro_dietikerComment #72
johnchqueAdded 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. :)
Comment #73
Berdirthat 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.
Comment #74
johnchqueOops, sorry, changed as serializing an empty array []. :)
Comment #75
johnchqueI can confirm that with the last patch there are no errors displayed, I was able to reproduce with the previous patch. :)
Comment #76
BerdirOk, then, lets do this ;)
Comment #78
miro_dietikerCommiting. 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.
Comment #79
Primsi CreditAttribution: Primsi at MD Systems GmbH for MD Systems GmbH commentedIn 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.
Comment #80
Primsi CreditAttribution: Primsi at MD Systems GmbH for MD Systems GmbH commentedDoh, overlooked #2834319: Add per-paragraph-type configuration
Comment #82
fgmFWIW, this commit broke one of our sites, with this stack (no custom code in it, as you can see):
Comment #83
BerdirI think there's an issue about that somewhere in the queue.
Comment #84
stBorchertI 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.