Motivation

  • Getting the constraint that are applied to a certain data or field definition is confusing and not easy, as $definition->getConstraints() only returns the explicitly specified ones. Right now, it's confusing on where and how type-based defaults are added in.
  • $definition->getClass() does not return the data type class which ends up being used, however that is what you usually need.
  • Listdefinition::getClass() already adds in the default class based on the list item type, so it's not consistent on how defaults are added in right now

Proposed resolution

Always apply defaults for getClass() and getConstraints() and document that.

Remaining tasks

-

User interface changes

-

API changes

  • DataDefinition::getConstraints(), DataDefinition::getConstraint() as well as their FieldDefinition equivalences add in type based defaults
  • DataDefinition::getClass() adds in the type based default

Original report by fago

Spin-off from #2047229: Make use of classes for entity field and data definitions. This issue creates classed definition objects which are used for defining fields and properties. Now, as we have classed definition objects the question comes whether they definition objects should be dumb and just return what has been set on them, or be smart and apply defaults (which might not be static but stem from data type's plugin defintion).

Example, data definition's have an optional 'class' property which may be set to override the data type's plugin class. Thus, if the class is not set, should $definition->getClass() return NULL or the data type's (default) class?

E.g. we could do

public function getClass($apply_default = TRUE) {
  // Get manager from container if not supplied.
  // Fetch type default from manager if no class is set.
}

If we decide to apply defaults the question comes up on how to get the manager best. I'd suggest doing a static helper method that reads from a public static variable on the class, what allows one to easily inject a manager for the whole class during unit-testing, while definitions continue to have no dependencies and can be easily serialized (what is critical!).

public static function typedData() {
  if (!isset(static::$typedData)) {
    static::$typedData = \Drupal::typedData();
  }
  return static::$typedData;
}

We've a similar situation as for 'class' with constraints. Right now we've

$definition->getConstraints(); // Gets any customly set constraints
$data->getConstraints(); // Short cut to the next
$manager->getConstraints($typed_data); // Applies defaults constraints and instantiates constraint objects

So, regardless of how we decide to move on with this issue we need to improve the confusion around those getCosntraint() methods, but depending on this issue we'll need a different solution - so let's clarify this first!

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

yched’s picture

Having definitions be the go-to objects for asking about information sounds like a nice idea in principle. That's basically what FieldDefinitionInterface::getSchema() is doing.

Some thoughts though:

- "Smart logic" like "merging from an outside source" raises the question of statically caching the results, meaning we then need to be cautious about serialization.

- Getters that are not symmetrical with setters feel a bit slippery API-wise. Means get(set()) is not stricly idempotent. You can only "get()" the merged version, not "what the caller of set() just placed in there". Shaky for stuff that ends up saved in Field / Instance config entities (you save the merged version, not strictly "the definition").
getSchema() escapes that, since that's purely derived data, with no associated setter.

- Having multiple potential sources for the info is indeed confusing. "Who should I ask then ? Who has which version ?" - e.g for constraints:
$data->getConstraints() ?
$definition->getConstraints() ?
$manager->getConstraints($data) ?
It's probably a bit clearer if $data::getConstraints() is a static - at least shows you're not supposed to call it through a $data object (although I guess it still shows up in IDEs autocomplete for "$data->" ?)

But right, settling on a pattern of :
static FieldItem::schema($field_definition)
static FieldItem::getPropertyDefinitions($field_definition)
static FieldItem::getConstraints($field_definition)
would make sense - although, er, would that mean schema() should be getSchema() ?

- We probably can't apply that pattern everywhere.
FieldDefinition::getDisplayOptions(), added in #2144919: Allow widgets and formatters for base fields to be configured in Field UI, for example: it only returns "striclty the defaults that were defined in setDisplayOptions()", not the actual resolved runtime display options, those are held in an EntityDisplay, we probably want to add EntityDisplays to the mix here :-)

Would it help if we made a list of "things" we'd want to apply that pattern to:
- class
- constraints
- schema
- ?

fago’s picture

Title: Clarify how classed definition objects deal with defaults » Apply defaults to definition objects
Issue summary: View changes

Based on #2175017: FieldDefinition::create() doesn't populate default 'settings' for the field type and recent discussions it appears that we want to do that.

I've been discussing with yched possible implementation approaches in IRC a bit. We mostly discussed two ways:
a) set defaults during object construction, then have usual get()/set()
b) apply defaults in the getters, but optional. e.g. getConstraints($apply_defaults = TRUE)

a) seems good to me in general, but has two problems:
- some defaults depend on other field definition settings, in particular constraints. Imo it would be nice when $definition->getConstraints() just gives you all the defaults, but that approach would not work when setting them during construction
- I fear setting all the default constraints will eat too much memory - as it will sit in all the serialized field definitions in memory

fago’s picture

So we figured we could regularly do a), while we handle constraints different and go with b) there.

tim.plunkett’s picture

See #2272161: Entity-based conditions should use type => entity:$entitytype instead of constraints for a), I think that can go in on its own, and we can handle b) here...?

fago’s picture

So a) is problematic due to memory issues for field definitions and having type-based constraints being alterable via the data definition, what shouldn't be the case. Therefor, I think we should go with variant b) here as mentioned in #3.

tim.plunkett’s picture

Okay, b) should work for my needs.

tim.plunkett’s picture

Status: Active » Needs review
Issue tags: +Page Manager
FileSize
2.75 KB
9.07 KB

Here we are. This test should illustrate why I had to postpone #2272161: Entity-based conditions should use type => entity:$entitytype instead of constraints.

The last submitted patch, 7: definition-defaults-2116341-7-FAIL.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 7: definition-defaults-2116341-7-PASS.patch, failed testing.

fago’s picture

+++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/ContextPluginTest.php
@@ -185,4 +186,26 @@ function testContext() {
+      $definition = new DataDefinition($context_definition);

That's wrong. DataDefinition is not the right class for all kind of types, in particular not for entities. use $typed_data_manager->createDataDefinition() and "entity:node" will give you all the wanted constraints.

fago’s picture

Status: Needs work » Needs review
FileSize
19.03 KB
17.39 KB

I figured the problem of #10 is in the Context class also. Problem is for fixing that we need an easy way to set the array-of-definition values on the resulting definition class - we do not have that right now. Not sure, it's reasonable to try to fix that though - shouldn't we use definition classes for context definitions in the first place? -> Let's discuss this in its own issue?

@Applying defaults to definition classes: I think we should default to applying defaults, as this is usually what you want. Also I moved all existing defaults from the manager into it and fixed the confusion with the naming of TypedDataManager::getConstraints() - updated patch attached.

fago’s picture

Fixed some docu glitches.

The last submitted patch, 11: d8_definition_defaults.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 12: d8_definition_defaults.patch, failed testing.

tim.plunkett’s picture

That's wrong. DataDefinition is not the right class for all kind of types, in particular not for entities. use $typed_data_manager->createDataDefinition() and "entity:node" will give you all the wanted constraints.

That's fine, but we need some form of test here, so that we can safely make the change in #2272161: Entity-based conditions should use type => entity:$entitytype instead of constraints

fago’s picture

Status: Needs work » Needs review
FileSize
18.11 KB
1.47 KB

ad #15: ok, so let's extend coverage over there?

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

Okay, sounds good.

I avoided making the default TRUE because I didn't want to track down all of those instances, but its definitely better.

tstoeckler’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs issue summary update

Coming here from #2236903: setSettings() on field definitions can unintentionally clear default values.

The issue summary does not at all clarify the use-case for setting $apply_defaults to FALSE. I.e. why this is needed in the first place. The boilerplate documentation in the patch does not reveal any information either, as to when I would want to use which. Marking needs work for that at least.

The confusing-ness of the different getConstraints() methods is a thing, but I'm not sure this is the solution. I.e. when I have a data definition and I want to get a list of constraints why would I only want those specifically set up for this particular field definition. I want all constraints that are applicable to this field definition. If I don't want the defaults to apply, I can remove them with setConstraints() (or if that doesn't work that's a separate bug, at least), but I don't see a use-case for this separation.

For getting a single constraint I find this even stranger, as a consumer, as there shouldn't be a difference between a constraint that is default or not default.

The places where this is changed in code are not commented, so that is not very helpful either.

fago’s picture

I want all constraints that are applicable to this field definition.

Right, this should $definition->getConstraints() give you. Right now, it does not.

If I don't want the defaults to apply, I can remove them with setConstraints() (or if that doesn't work that's a separate bug, at least), but I don't see a use-case for this separation.

The defaults are not optional, they apply always. However, you can use getConstraints() to get the constraints set on the field definition only.

For getting a single constraint I find this even stranger, as a consumer, as there shouldn't be a difference between a constraint that is default or not default.

hm, one could argue that $apply_defaults should not exist in the first place, yes. But then we need to watch out to keep the methods idempotent, i.e. you get what you set? I could see improving documentation be enough here, plus then this would work inline with #2236903: setSettings() on field definitions can unintentionally clear default values also.

fago’s picture

Status: Needs work » Needs review
FileSize
16.67 KB
6.88 KB

ok, so here is a patch following #19, i.e. always add defaults and document that.

yched’s picture

Can't really formulate whether it's connected to this needs for "with defaults / without defaults", but I have a vague feeling that we're using the same API for two things here:
- get a list of "constraint definitions" (arrays) - either those that are implicitly enforced by the TypedDataManager, and / or those that are additionally explicitly set by the data definition at hand.
- get a list of actual, runtime Constraint objects, always instantiated from *all* of above, in order to feed to the Validation API.

The fact that the @return phpdoc for some xxConstraint() methods mentions "Constraint objects", and others "arrays ready to create Constraint objects" feels a bit like an API smell.

Again, sorry, a bit vague, maybe it's not related at all, feel free to ignore.

fago’s picture

I don't think it's directly related, but I think it's mostly TypedData::getConstraints() which is different. I'd love to do away with it by having the constraint codeflow as suggested for default values (primary source = definition object), then the metadata-factory of the validator can take over creating the constraint objects and we handle constraint definition arrays only?

fago’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update +beta target

I've updated the issue summary. Also this should go in before beta.

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

I think the points of #21 are valid, but I don't think this issue makes the situation that much worse.
I think switching according to #18/#19 was the right way to go, thanks @fago!

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 20: d8_definition_defaults.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
16.46 KB

Straight PSR-4 reroll

catch’s picture

26: typeddata-2116341-26.patch queued for re-testing.

fago’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
16.28 KB
1.14 KB

I noted that there is an $apply_defaults parameter missing - rerolled the patch and removed it.

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

Good looking out.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.x, thanks!

  • Commit 3bbaaa9 on 8.x by catch:
    Issue #2116341 by fago, tim.plunkett: Apply defaults to definition...

Status: Fixed » Closed (fixed)

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

andypost’s picture

there's still 2 @todo left in code

$ git grep 2116341
core/lib/Drupal/Core/Field/BaseFieldDefinition.php:63:    // @todo Cleanup in https://drupal.org/node/2116341.
core/lib/Drupal/Core/Field/BaseFieldDefinition.php:476:   * https://drupal.org/node/2116341.
yched’s picture

This was marked fixed but BaseFieldDefinition::create() still has a "@todo cleanup in [this issue]" ?

// Create a definition for the items, and initialize it with the default
// settings for the field type.
// @todo Cleanup in https://drupal.org/node/2116341.
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$default_settings = $field_type_manager->getDefaultStorageSettings($type) + $field_type_manager->getDefaultFieldSettings($type);
$field_definition->itemDefinition->setSettings($default_settings);
yched’s picture

Also in BaseFiedDefinition (although that one is more "@todo cleanup after [this issue] is fixed"

/**
   * Helper to retrieve the field item class.
   *
   * @todo: Remove once getClass() adds in defaults. See
   * https://drupal.org/node/2116341.
   */
  protected function getFieldItemClass() {
    if ($class = $this->getItemDefinition()->getClass()) {
      return $class;
    }
    else {
      $type_definition = \Drupal::typedDataManager()
        ->getDefinition($this->getItemDefinition()->getDataType());
      return $type_definition['class'];
    }
  }
benjamin.merkley’s picture

Still not completed still see @todo in the code

apaderno’s picture

I opened a follow-up for what reported in the last comment.