Updated: Comment #0
Background
The FieldTypePluginManager is a DefaultPluginManager and thus it implements FactoryInterface's createInstance() method. It is a common pattern among plugin managers that you can call their createInstance() method to receive a plugin instance.
Problem/Motivation
Call FieldTypePluginManager::createInstance() with a proper FieldType plugin ID.
Expected result:
Get a field type instance.
Actual result:
Fatal error.
Note: To me it is pretty major that you can fatal by using the API - and not just any API: The plugin API one of the central APIs of Drupal 8. I realize, however, that our current usage of issue statuses do not match the actual meaning of the words, "major", etc. so I leave it to others to fight over that.
Details
So given the above how are FieldType's instantiated in Drupal currently? Meet TypedDataManager!
TypedDataManager::createInstance() allows to pass in an object in $configuration['data_definition'] and it will then call a getClass() method on that to get the class to instantiate. That means you can instantiate anything that has a getClass() method with TypedDataManager!
The reason for this is the flexibility of the TypedDataSystem. An ItemList, for example, has the ability to instantiate FieldItem properties. I.e. you would think it has to be a FieldItemList in that case but that's not necessarily true, as FieldItemList does not override createItem(). So TypedDataManager::createInstance() needs to be able to instantiate both DataType's and FieldType's.
Proposed resolution
- Change FieldType plugin constructors to look like any other plugin constructor __construct(array $configuration, $plugin_id, array $plugin_definition, $whatever_this_plugin_needs)
. While that is not a requirement of the plugin system it is pretty consistently done that way throughout core currently, so we should really be consistent.
Implement FieldItemList::createItem(). I'm not sure if the item list example from above is the *only* example of the dynamic class instantiation. Given the general abstract-ness of the typed data system I would assume that's not the case. If it is however, we could easily fix this problem by calling FieldTypePluginManager from FieldItemList::createItem(). That would no longer allow to have FieldItem's be in an ItemList (and not a FieldItemList), but is that really something we need to support?
Remaining tasks
API changes
Comment | File | Size | Author |
---|---|---|---|
#60 | interdiff-58-60.txt | 729 bytes | mpdonadio |
#60 | fieldtypepluginmanager-2227227-60.patch | 17.87 KB | mpdonadio |
#58 | field-item-plugin-manager-2227227.58.patch | 17.86 KB | swentel |
Comments
Comment #1
chx CreditAttribution: chx commentedEdit: the only reason I participate in this because I thought it directly affected my MongoDB work, trying to access the schema of the field types so that I can properly typecast when searching. I *bet* entity query is broken on postgresql too cos of strict typing. I have worked around this and now it doesn't block me but my first attempt was to instantiate the field plugin and call schema(). That doesn't work -- schema() also needs a field definition I do not know where to get from so it's pretty much futile and unnecessary anyways:
this works.
I elevated this to major because the TypedData codeflow can be a little challenging to follow and having a plugin manager instantiating not just its own plugins but other plugin types and non-plugin classes too (FieldItemList) is really confusing.
Comment #2
tstoecklerSo I talked to @fago about this yesterday. Will need to think some more about this and maybe play around a little with different solutions.
The first part of the "proposed solutions" part is incorrect per @fago. A DataDefinitionInterface instance is actually the definition of the concrete data object, not of the data type. I.e. it is comparable to $configuration as it is being used elsewhere and not $plugin_definition. In other words:
DataTypeInterface -> $plugin_defininition
DataDefinitionInterface -> $configuration
This doesn't change the fact that the current wording is very confusing and also $configuration is being type-hinted as array all over the place. So we still need to resolve this somehow, it's just that semantically this is consistent(ish) with other plugin managers where I previously thought the order was just wrong/inconsistent.
Comment #3
tstoecklerAlso @fago mentioned that the reason that currently everything goes through TypedDataManager is that there is a performance implementation there. So we need to retain that in some way.
Comment #4
XanoCan we please mark this a beta blocker, as it's highly confusing and, to use a buzzhashtag, a #DrupalWTF.
Comment #5
alexpottThis does not need to be a beta blocker but it can be critical. Using an API as expected and getting a critical is not right.
Comment #6
yched CreditAttribution: yched commentedYes, that's a long standing gripe with TypedData.
This is a subtopic of #2002138: Use an adapter for supporting typed data on ContentEntities, though.
Comment #7
BerdirComment #8
Jose Reyero CreditAttribution: Jose Reyero commentedAgree with @chx
And I would add everything about how plugins are instantiated in general is really confusing. To start with we are instantiating plugins using some constructor that is not part of any interface.
Also everything that extends TypedData like FieldItemBase has atm some hidden dependency on TypedDataManager. See validate() or Map::get() methods.
From #3
I think that shouldn't be an excuse, we should fix the issue first (API, architecture), worry about performance optimizations later.
Btw, if we need such optimizations, then maybe we need them for all plugin managers? Shouldn't they be part of DefaultPluginManager?
About this issue, could we fix it getting FieldtypePluginManager to extend TypedDataManager, like we did with TypedConfigManager, so at least each manager instantiates its own plugins?
Then we just need to fix that 'hidden dependencies' so each manager can also handle its own plugins.
Comment #9
yched CreditAttribution: yched commentedBeen a while wince I last struggled with TypedDataManager, and this would totally need @fago's feedback, but solution B (having the DatatDefinition objects be in charge of instanciating their own Data objects) sounds like an interesting approach. We didn't have DatatDefinition inititally.
Then,
- Each DataDefinition defers to the PluginManager it's associated with (FieldTypePluginManager, whatever else). This what does the interface between the PluginManager, and the TypedData system.
- TypedDataManager can still be the go-to "create anything TypedData-ish with "cloned prototypes" optimizations to reduce the number of objects to construct", for regular runtime code like ContentEntity. It is basically a meta-factory that delegates to several actual PluginManagers, through the DataDefinition objects.
- For one-off, non-optimized creation you can directly call the right PluginManager (e.g. FieldTypePluginManager), they work just as you would expect from any PluginManager.
- Hopefully (not sure), it would remove the need to duplicate the "field type" plugin definitions into the "data type plugin definitions". Currently, "field type" plugin definitions are discovered by FieldTypePluginManager, and then, using FieldItemDeriver, re-exposed as "data type" plugin definitions, resulting in duplicate data in persistent cache and in memory cache.
I *think* TypedDataManager would just need its plugin definitions to provide a 'definition_class', without duplicating the rest of the definition.
Comment #10
andypostBut there's related issue with that cloned prototypes #2221577: Fix assumption that field settings is not a nested array
Comment #11
Jose Reyero CreditAttribution: Jose Reyero commentedThis doesn't sound very well to me, then we are making it into an object factory, will need to handle injected dependencies, etc...
IMO DataDefinition should just tell the 'Manager' how to instantiate the plug-in which is what it's doing atm, right?
Is data definition associated with a plugin-manager?
This sounds interesting, though I think it will work better the other way around. It should be each plugin manager delegating to TypedDataManager.
How I think it should work:
- Each Plugin Manager handles plugin definitions, builds data definitions, etc..
- Plugin Managers delegate actual object creation to TypedDataManager (that should be an injected dependency for them).
But for this we need to:
- Work out a common interface for building these objects. And a constructor is not such a thing.
- Remove static dependencies from TypedData objects on TypedDataManager.
Really I would be more than happy having TypedConfgManager delegating to TypedDataManager instead of extending it. But the issue I've bumped into is TypedDataManager needing to use its own definitions for creating objects:
Then if it needs to have a definition for it -though I don't get what for-, otherwise it throws an exception, it just won't work for 'my' typed config' plugins, that are defined somewhere else.....
I don't know, maybe the issue with other managers is different... ?
Comment #12
fagoThis relates pretty much to #2002138: Use an adapter for supporting typed data on ContentEntities, in particular it's sub-issue #2268009: Make typed data object instantiation more flexible . As discussed several times, I very much agree that field items should be instantiated via the field type manager primarily, while typed-data plugins can implement some sort of adapter to typed data's generic creation method.
For that to really work out we need to move over the prototyping approach to the field type manager as well, or at least make the TDM-version usable by the field type manager and proxy the calls through the field type manager.
Interesting idea, not sure it belongs there though. While a short-cut to create instance from the definition makes sense there, I'm not sure the instantiation logic belongs there also. It does not feel right to have to customize a definition class if you need to customize instantiation.
While working on #2002138: Use an adapter for supporting typed data on ContentEntities I started adding a custom static create() method directly to the data type class.
Comment #13
tstoecklerThe day has come that I finally return to this issue...
Coming back to this issue after a long time, I think I might have a rather simple solution to fixing the problem at hand. I still think a lot of the typed data instantiation process is backwards and weird (probably because I do not understand most of it) but we don't have to fix everything at once. This just offloads
TypedDataManager::createInstance()
into aTypedDataFactory
whichFieldTypePluginManager
then uses to instantiate field types.Comment #15
tstoecklerOh right, there is a thing called
TypedConfigManager
.Drupal 8 and its inheritance chains are just so much fun....
Comment #16
chx CreditAttribution: chx commentedThanks! Please add a test.
Comment #17
yched CreditAttribution: yched commentedLooks legit indeed - awesome :-)
@fago should probably vet this though. Assigning to him.
Comment #18
yched CreditAttribution: yched commentedWhy do we remove passing $plugin_interface = 'FieldItemInterface' to the parent __construct() though ?
Nitpick: I guess we could add a word of comment that "field type plugins" are FieldItem data objects, and are thus created by the TypedDataFactory ?
[edit : and also that in the regular lifecycle of an entity, they are created by TypedDataManager rather than FieldTypePluginmanager...]
Comment #19
yched CreditAttribution: yched commentedOh, right - because $plugin_interface is only used by ContainerFactory in the DefaultPluginManager.
Is there a way we could add such a $plugin_interface check to TypedDataFactory ?
Comment #20
chx CreditAttribution: chx commentedNote that while this does fix FieldTypePluginManager which is great it doesn't fix the underlying issue which is TypedDataManager instantiating field type plugins. FieldTypePluginManager is still unused. This is probably a non-critical followup which I expect never will be fixed :/
Comment #21
dawehner@tstoeckler
Now I know what you tried to talk with me about yesterday.
This is quite a cool solution, as it solves at least one crazyness you get while reading the code.
Comment #22
xjmComment #23
fagoI'm not sure introducing the TypedDataFactory makes sense, as it promotes an anti-pattern - other plugin manager shouldn't re-use it. What about injecting TypedDataManager as dependency instead?
I'll take a look at it.
Comment #24
fagoWhat about something like that?
Patch generally works, but probably will require some unit test adaptions. Feedback welcome first. I also changed the calls from the entity / field classes so they only talk to the field type manager, what makes sense but adds another method call.
Comment #25
fagoComment #27
yched CreditAttribution: yched commentedYummy, the actual runtime creation of the FieldItemList and FieldItem objects within an Entity actually goes through the FieldTypePluginManager - this is lovely indeed :-)
It is confusing that the param order is inconsistent between the two methods :
- with createFieldItemList(), the parent (the Entity) comes last
- with createFieldItem(), the parent (the List) comes first
Related, it's insconsistent that:
- "create an Item with or without a parent" is two separate methods
- "create an ItemList with or without a parent" is the same method with an optional param ?
The following would fix that ?
createFieldItem($items, $index, $values = NULL)
createFieldItemWithoutParent($definition, $values = NULL)
createFieldItemList($entity, $field_name, $values = NULL)
createFieldItemListWithoutParent($definition, $values = array())
Comment #28
fagook makes sense, renamed accordingly and moved methods to the interface. Still needs test fixes and added test-coverage for the new methods.
Comment #29
yched CreditAttribution: yched commentedSorry for letting this slide. Seems moslty good.
IMO we should add a word that items/itemLists without parents are invalid for most regular use cases, and that the "with parent" methods should be used in general be used instead.
In fact - do we really need to add those two methods to begin with ?
(phpdoc of createFieldItemList($entity))
Shouldn't we say something similar for createFieldItem($list) then ? Likewise, it creates an Item with the $list as parent, but doesn't take care of adding the item to the list ?
Missing newline
Comment #30
larowlanFixes #29
Also test-run in current state
Comment #32
yched CreditAttribution: yched commentedI somehow missed #30, thanks @larowlan :-)
Patch seems to apply with fuzz, rerolled.
If green, this looks ready to me.
Comment #34
dawehnerI was wondering whether core could leverage the various methods on the field type manager?
This line itself makes things much easier to understand.
Comment #35
amateescu CreditAttribution: amateescu commentedSome unit tests had to be updated.
Comment #36
yched CreditAttribution: yched commentedYay, @amateescu++
Then we can replace the:
return \Drupal::typedDataManager()->getPropertyInstance($items, 0);
that #2164601: Stop auto-creating FieldItems on mere reading of $entity->field[N] just added in getOptionsProvider() in FieldStorageConfig and BaseFieldDefinition with
return \Drupal::get('plugin.manager.field.field_type')->createFieldItem($items, 0);
Leaving that to a good soul, so that I can still RTBC ;-)
Comment #37
amateescu CreditAttribution: amateescu commentedFeeling brave tonight :)
Comment #39
amateescu CreditAttribution: amateescu commentedWell... this is what I get for copy-pasting code without even reading it :/
Comment #40
yched CreditAttribution: yched commentedHah, right, sorry about that :-)
Thanks! Here we go then...
Comment #41
alexpottBoth of these methods appear to be untested and used. Why do we need them? And if we do can we add tests?
Comment #42
yched CreditAttribution: yched commentedYeah... as mentioned in #29.1, I'm not sure why we need those, or if it's really an API we want to provide. Since they have no use case in core atm, I suggest we remove them from the patch, and discuss whether we want them in a separate issue.
Patch removes them, and adjusts the phpdoc for the remaining methods (fixes a couple inaccurracies I didn't notice so far, tweaks phrasing)
I'll be bold and put this back at RTBC, feel free to slap my wrist and ask for someone else to push the button :-)
Comment #43
yched CreditAttribution: yched commentedReordered methods - conceptually "create item list in an entity" comes before "create item in an item list"
Kept that for a separate patch for easier interdiffs.
Comment #44
alexpottI don't think we have any implicit or explicit test coverage of this... which is the whole point of the issue.
Comment #45
mpdonadioI think I will have some time this afternoon to add something to FieldTypePluginManagerTest to cover this.
Comment #46
mpdonadioHere is a first pass at a test.
Comment #47
dawehnerI would vote to use
String::format
by default, if possible.On top of it I wonder whether we can get some more values to ensure that for example the configuration is passed along correctly?
Comment #48
mpdonadioOK, not totally sure what you had in mind for additional config, but here are some changes. This is going to fail.
Since FieldTypePluginManager::createInstance()
There is no parent, so FieldItemBase::getFieldDefinition() and friends barf b/c the ->getParent() is baked into those calls. Not sure what the proper thing to do is, or is I just goofed my test config.
Also, should FieldTypePluginManager::createInstance() and the associated docblock be added to FieldTypePluginManagerInterface?
Comment #50
dawehnerIn order to fix the failure we probably have to adapt FieldItemBase to not require a parent all the time, so probably check whether the parent
returns something, and in case it doesn't , use just
$this->definition
?Comment #51
mpdonadioI'll play with this and see what breaks...
Comment #52
mpdonadioUnassigning myself. I haven't made any progress on this, and don't have anything worthwhile to post as an in-progress patch.
Comment #53
larowlanPerhaps this?
Comment #54
dawehnerwhy is that change needed
Comment #55
dawehneryou could store parent in the if
Comment #56
swentel CreditAttribution: swentel commentedaddressed 54 - also changed the number in the assert message which was different :)
re: #55 - getDefaultValue() needs the entity - which why it failed in #48 - I think.
Comment #57
yched CreditAttribution: yched commentedHm - then why don't we just always return $this->definition->getFieldDefinition() ?
Comment #58
swentel CreditAttribution: swentel commentedHmm, good point
Comment #59
dawehnerGood idea @yched
The issue summary still describes multiple possible options, let's remove the other one, I think.
Nitpick: Let's make it public, please ... there are instances which are protected, which is kinda problematic, see #2443381: Make all test functions public
Comment #60
mpdonadioAdded explicit public declaration (though by default it was already public).
Cleaned up the IS a bit, and hid older files.
Comment #61
dawehnerThank you!
Comment #62
alexpottThis issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed b0f4b41 and pushed to 8.0.x. Thanks!