Follow-up to #2030637: Expand FieldConfig/BaseFieldOverride/FieldConfigBase with methods
Part of meta-issue #2016679: Expand Entity Type interfaces to provide methods, protect the properties.
Drupal\Core\Field\FieldConfigBase class variables should not be accessed directly. There is only one variable FieldConfigBase::default_value left to set protected.Functions should be use to access the variable. For instance use getDescription() and setDescription($description) for the protected class variable description. For a boolean variable the getter function becomes isVariableName(). In object-oriented programming this is called encapsulation.
Remaining tasks
- Update the class variables and make them protected.
- Create getters and setters for frequently used get and set functionality.
- Update drupal to use the getters and setters instead of accessing variables directly.
- There are no tests required because the added functions are only getters and setters.
For more info over what should be done see the issue summary of #2016679: Expand Entity Type interfaces to provide methods, protect the properties.
Maintainer decision
The naming of getter and setter function is getNameOfTheVariable() and setNameOfTheVariable(). For the variable $default_value would that result in getDefaultValue() and setDefaultValue(). The setter function is already in core and sets the value of $default_value. The getter function is also in core and is used for something else. There are three viable options from which we can choose:
- Keep the function getDefaultValue as is and use an other name for the getter function. For instance getTheDefaultValue();
- Rename the current function getDefaultValue() (to for instance getDefaultValueEntity()) and use getDefaultValue for the getter function.
- Combine both functions in getDefaultValue(). If it has a parameter then use it as the current function and if there is no parameter use it as the getter function (
function getDefaultValue(FieldableEntityInterface $entity = NULL)
).
What we need is a maintainers decision for which solution to choose.
Beta phase evaluation
Issue category | Bug because properties should not be public, API methods should not be allowed to be sidestepped. |
---|---|
Prioritized changes | Prioritized since it is a bug and it reduces fragility. |
Disruption | Somewhat disruptive for core as well as contributed and custom modules:
|
But impact will be greater than the disruption, so it is allowed in the beta.
Comment | File | Size | Author |
---|---|---|---|
#48 | interdiff.txt | 2.08 KB | yched |
#48 | 2529034-field-config-base-default-48.patch | 28.06 KB | yched |
Comments
Comment #1
daffie CreditAttribution: daffie commentedComment #2
penyaskitoRolling the ball. FieldConfigBase::getDefaultValue($entity) already exists, so it's hard to name properly how to get the default value for the field config. Suggestions welcome.
Comment #3
penyaskitogetTheDefaultValue() has been added to FieldConfigInterface, but maybe should it be on FieldDefinitionInterface?
Comment #5
penyaskitoChanging a missed reference to the property.
Comment #6
andypostMaybe now it's possible to make comment field default value with this method... follow-up
Comment #8
penyaskitoFix failures, missed some accesses to the variable.
Comment #9
Mile23So
FieldDefinitionInterface
already has agetDefaultValue()
(no 'the'). ThenFieldConfigInterface
hassetDefaultValue()
which defines its parameter as the output ofFieldDefinitionInterface::getDefaultValue()
.FieldConfigBase
then implementsFieldDefinitionInterface
, which as you can see accessesdefault_value
according to some logic.This means that
FieldConfigBase::getDefaultValue()
andFieldConfigBase::setDefaultValue()
already implements the encapsulation we desire, or at least should.This patch should not define
getTheDefaultValue()
and should instead focus on having calling code use the interface rather than the public method.Comment #10
penyaskitoFieldDefinitionInterface::getDefaultValue
receives anFieldableEntityInterface
, which sometimes we don't have if working directly with field definitions. That's why I defined a new getter. That entity is needed because we call later to::processDefaultValue
, which needs the entity.Should we ignore that if the entity is null?
Comment #11
Mile23I guess I really don't know. :-)
Having a
setDefaultValue()
and also asetTheDefaultValue()
seems rather confusing in terms of DX.Comment #12
penyaskitoLet's try allowing NULLs then.
Comment #13
penyaskitoOops, rebased with 8.0.x
Comment #14
penyaskitoYay
Comment #15
daffie CreditAttribution: daffie commentedWe need a maintainers decision which function naming we should use. I have updated the IS with the maintainers options as I see them.
Comment #16
yched CreditAttribution: yched commentedSo, summarizing:
The runtime default value of a field can be specified in two different ways, each with its associated setter :
- default_value / setDefaultValue() : a litteral value,
- default_value_callback / setDefaultValueCallback() : a callback, possibly accepting the host $entity as a param.
Those are currently combined in a single "getter", getDefaultValue(), whose intent is to compute the runtime default value for a given field in a given entity, based on one of the two underlying properties (callback if present, else litteral). It is thus not really a getter for the two basic properties above, instead it smushes/resolves them to a runtime litteral.
And there are no real getters for the underlying properties (litteral / callback)
--> I'd suggest we keep getDefaultValue() for what it currently is, and add separate, dedicated getters for those two params :
FieldDefinitionInterface::getDefaultValueLitteral() / getDefaultValueCallback(),
with proper docs stating that getDefaultValue($entity) should be used instead for determining the actual runtime default value for a given field in a given entity.
Then we'd have :
setDefaultValue($value) : set as litteral value
setDefaultValueCallback($callback) : set as a callback
getDefaultValueLitteral() : get the litteral value
getDefaultValueCallback() : get the callback
getDefaultValue($entity) : get/compute the runtime default value for an entity
It's not entirely symmetric (setDefaultValue() is the setter for which getDefaultValueLitteral() is the getter), but IMO that's still the most intuitive DX solution. I'm not sure we want to rename setDefaultValue() to setDefaultValueLitteral() at this point.
Also, I notice that FieldConfigBase currently misses the setDefaultValueCallback() setter for default_value_callback (BaseFieldDefinition has it). That was probably an overlook in #2030637: Expand FieldConfig/BaseFieldOverride/FieldConfigBase with methods, we could add it here as well.
Comment #17
penyaskitoMakes sense to me, wondered if we wanted to really expose those methods.
This patch covers what yched asked for if I understood correctly.
Comment #18
yched CreditAttribution: yched commentedThanks @penyaskito ! A couple remarks :
Why do we redefine this here ? It's already defined in FDI, which FCI extends.
What we need to add in FCI is setDefaultValueCallback(), see next item below :-)
Great, but the @inheritdoc is a lie atm :-), the method is not defined/documented in any interface currently implemented by FieldConfigBase
We need to add it to FieldConfigInterface, that's where the various setters live atm.
Minor, code organisation : the rest of the class tries to group getters and setters together (getters first), let's do the same here.
Not sure of our standards here, but shouldn't we add a trailing () after the method name ?
For completion and clarity, I'd suggest that crossreference getDefaultValueLiteral(), getDefaultValueCallback(), getDefaultValue() all cross-reference each other. Something like :
- getDefaultValueLiteral() / getDefaultValueCallback()
"This method retrives the raw property assigned to the field definition. When computing the runtime default value for a field in a given entity, ::getDefaultValue() should be used instead."
- getDefaultValue() :
"This method computes the runtime default value for a field in a given entity. To access the raw properties assigned to the field definition, ::getDefaultValueLiteral() / ::getDefaultValueCallback() should be used instead.
The isset() looks weird now that we extract a variable. An is_null() would be more explicit ?
In fact, now that we have getters / setters, we could ensure that we always get and set array() instead of NULL, and remove that check here.
The phpdoc in HEAD for FieldDefinitionInterface::getDefaultValue() already states that it always returns an array, empty for "no default".
The phpdoc for the newly added FieldDefinitionInterface::getDefaultValueLiteral() says the same, so it should already be the case ?
If we enforce the getter always returns an array, the is_array() check shouldn't be needed, and we could just do
if ($default_value = $this->getDefaultValueLitteral())
Same here, could be if ($default_value = ...). And the "if ($changed)" block at the end should move inside that first if(), after the foreach().
Minor, but we don't really *need* to extract a var, this could stay inlined. Feel free to ignore :-)
Comment #19
penyaskitoThis one should make @yched happier :-)
Comment #20
yched CreditAttribution: yched commentedThanks @penyaskito. A couple more details :-)
Actually, according to the convention and PHPdoc, the "not set" case should return [] rather than NULL ?
+ just like you did in FieldConfigBase, the getDefaultValue() method in BaseFieldDefinition could call getDefaultValueLiteral() rather than duplicating that line ?
(actually, if getDefaultValue() only relied on getDefaultValueLiteral() / getDefaultValueCallback(), it could be moved to a trait and shared between FieldConfigBase & BaseFieldDefinition. Let's call that a followup though, I think there are more common runtime logic that could be shared in a trait between the two)
That's incorrect for an incoming $value === NULL, this will end up assigning a default value with NULL in the first property.
We should be converting NULL to [] here, as getDefaultValueLitteral() does - in fact, no need to do it in both places, it should be enough to do it here in the setter.
Likewise, no need to do the "normalize scalar to array using the first property" processing in both the getter and setter, doing it in the setter should be enough (we probablky need to keep it in getDefaultValue() that does "compute callback or litteral", since it currently allows the callback to return NULL or a scalar, we don't want a BC break here)
Similar "non-duplicate processing / single-point of entry" logic should probably be checked in the equivalent methods in BaseFieldDefinition too ?
Comment #21
penyaskitoOh, good catch. I double-checked that the default value is an empty array and it cannot be set to a NULL value.
Removed the logic in getters and reviewed it on setters on FileConfigBase and BaseFieldDefinition.
Comment #23
penyaskito@yched So this is an API change, or should we allow NULLs then? I'm not sure I got it right.
Comment #24
yched CreditAttribution: yched commented@penyaskito: we should keep allowing NULL, but have the setter internally translate it to [] (empty array) before assigning it to a definition property. That preserves BC.
Comment #25
penyaskito@yched: The test that fails is because after that, getting the value will return array() instead of NULL, is that right and should we change the test, or if the property is an empty array we should be returning NULL?
Comment #26
yched CreditAttribution: yched commentedYes, the getter returns [] for "no default value", that is what its phpdoc says.
"empty array" is the official value for "no default value". The setter accepts NULL instead for BC, but internally we always store [].
Comment #27
penyaskitoChanged the test expected output then.
Comment #28
yched CreditAttribution: yched commentedThanks @penyaskito ! Looks good.
This will likely need a change record though.
Comment #29
alexpottIs there no $default_value_callback property to protect? Hmmm... yes and it is already protected. We still have public access of it...
in
Drupal\Core\Field\FieldItemList::defaultValuesForm()
andDrupal\datetime\Plugin\Field\FieldType::defaultValuesForm()
. Is this broken atm?$value should be $callback like the interface.
Comment #30
yched CreditAttribution: yched commented#29.2 - indeed. That doesn't fatal out because it's inside an empty() check, which eats the incorrect property access and always returns TRUE (see http://3v4l.org/9A8aB), but the underlying functionnality (don't show the UI to set a litteral default value when there is a default value callback) is indeed broken.
Comment #31
penyaskitoFixed both access to default_value_callback. Should we have tests covering that?
Fixed also the argument name.
Comment #32
penyaskitoDrafted a change record.
Comment #33
yched CreditAttribution: yched commentedYeah, I guess we miss a test that checks that if a FieldConfig has a default_value_callback set, then the "Edit Field" UI doesn't show the "default value" input widget :-/
Comment #34
penyaskitoWIP of the test for that. There are some errors related to providing config schema, but aside of that it should work.
Comment #36
vijaycs85few minor points:
Minor: Would be nice to have both setter/getter same naming. Do you see anything wrong with getDefaultValue()?
Do we really need this check? If the value is empty, don't we get warning already?
May be we can just stick with old isset()
Doesn't look like this is scope of this issue.
is it safe to assume that 'status' going to be there?. it's not new change, just wondering...
Minor: thought we follow full class name?
visible? for 'No field'
May be we can add resetDefaultValue() ?
Note: passing just NULL as argument set it as array.
Comment #37
penyaskitoThanks for reviewing, Vijay!
1.
getDefaultValue
already exists and have logic, so we need something for accessing the raw value. See #16.2. We allow NULL values as a default value, so API consumers could pass that. Internally that must be translated to an empty array.
3. Probably, but makes no harm as this line was already modified. Do you want to change it back?
4. Well, does are tests and we now comment value columns, so I would say it is safe.
5. Good find, changed it.
6. Oh, thanks. Changed the assert text.
7. I don't think it will be a common use case aside of using it on tests.
Vijay, maybe you could take a look at the test error? It is related to config schema and I'm not really sure how we should define the field config schema.
Comment #39
vijaycs85@penyaskito, I guess, the save() just returns status (ref: \Drupal/Core/Config/Entity/ConfigEntityStorage.php:299).
Comment #40
penyaskito@vijaycs85: I didn't expect that inconsistency between content entities and config entities.
However, I changed that while doing some cleanup, I expect #39 to fail for the said reason: config schema of the field.
Comment #41
vijaycs85It doesn't look like content entity returns the object either:
Drupal/Core/Entity/ContentEntityStorageBase.php:211
Anyways, let's see what testbot says.
Comment #43
penyaskito@vijaycs85 Sorry for my confusion and the misunderstanding. What I would love you to look is at the error we have in the latest one:
Thanks!
Comment #44
vijaycs85sorry for the delay @penyaskito. Looks like we got two problems. one with default_value and another one with default_value_callback.
1. default_value is an easy one. It just need another array before the
['value' => 'value']
. you can see this at Drupal/Core/Field/FieldConfigBase::setDefaultValue()2. There is an assumption that the callback should be a string:
I can see this has been implied at Drupal/Core/Field/BaseFieldDefinition.php
I tired to make the callback as static and represent the method as string 'ClassName::method', but it's throwing error:
Anyway, I am attaching 2 patches, one without callback test and all green and with callback test which is failing.
Comment #46
yched CreditAttribution: yched commentedgetDefaultValue() uses call_user_func() for the default_value_callback, and yes, the setting has to be a string, so either a regular function name or 'namespacedClass::staticMethod' should work.
So I'm not sure why
$field_config->setDefaultValueCallback('Drupal\field\Tests\FieldDefaultValueCallbackTest::calculateDefaultValue')->save();
in @vijaycs85's 2529034-44-with-callback-fail.patch doesn't pass. I'd suggest trying with an absolute namespace (adding a leading '\' before 'Drupal\field\...') ?Comment #47
yched CreditAttribution: yched commentedNote that this will slightly conflict with #2552799: FieldType with no available widget causes Fatal error. Depending on which gets in first, the other one will need a reroll
Comment #48
yched CreditAttribution: yched commentedTried my suggestion in #46 :
but that still causes the same fatal as @vijaycs85 describes in #44 : "Class 'Drupal\simpletest\WebTestBase' not found in FieldDefaultValueCallbackTest.php line 20"
If put the static callback in *another* class than the test class itself (say, FieldConfigBase, random pick) things work fine :
- if I manually edit field.field.node.article.body to put :
default_value_callback: 'Drupal\Core\Field\FieldConfigBase::calculateDefaultValue'
then the default value works as expected (node/add/article shows "Calculated default value" prefilled in the Body widget)
- If I modify the test with :
$field_config->setDefaultValueCallback('Drupal\field\Tests\FieldDefaultValueCallbackTest::calculateDefaultValue')->save()
then the test is green.
I tried using a static method in a different class in the same FieldDefaultValueCallbackTest.php file, that fails as well.
So it looks like this is only when referring to a class in the test file itself... Weird.
Fixed that by moving the method to a different class in a separate file.
Comment #49
yched CreditAttribution: yched commentedComment #50
yched CreditAttribution: yched commentedGreen, I'll allow myself to set back to RTBC since I only contributed a small test fix in the last patch.
Comment #51
penyaskitoI'm happy with it too. RTBC+1
Comment #54
daffie CreditAttribution: daffie commentedBack to RTBC.
Comment #55
alexpottI think this is the final public property on a config entity in core and as such it is a complete outlier and making it protected before we get to RC seems worth the probably limited disruption it will cause.
Committed dd8f566 and pushed to 8.0.x. Thanks!
Comment #57
yched CreditAttribution: yched commentedYay, that's one nice WTF going away. Thanks @penyaskito !
Comment #58
jibran:/ https://www.drupal.org/pift-ci-job/31301
Comment #59
penyaskito@jibran Sorry for that. https://www.drupal.org/node/2546598 should be helpful.
Comment #60
yched CreditAttribution: yched commentedAdjusted the wording of the CR a bit :-)